> The biggest problem I see with people trying to write unit tests is that they don’t want to change how they write code. They just want tests for it. It’s like watching an OO person try their hardest to write OO code in a functional language.
The biggest problem I see with people advocating for tests and employing TDD is that they do change how they write code to accommodate tests. This leads to inclusion of lots of unnecessary abstraction and boilerplate patterns that make code less readable and more bug-prone. OO world has spawned numerous non-solutions to turn your code inside-out so that it's easier to mock things, at the expense of code quality itself.
That said, if you go for functional style in OOP, i.e. shoving as much as you can into static helper functions and most of the rest into dumb private stateless functions, you suddenly gain both a clean architecture and lots of test points to use in unit tests. So you can have testable code, but you have to chill out with the OOP thing a bit.
> as much as you can into static helper functions and most of the rest into dumb private stateless functions
In our work we use C# and it is very hard, even next to impossible to make a static class pass a code review - given it's not for extension methods (which I hate... why not be explicit about the first parameter and stop acting as a part of the class </rant>). They just tell us to use IoC and move to the next point. I honestly don't know why. Our IoC library can treat a dependency as static or singleton, but those are also discouraged. Once I had a static class named GraphRequestHelpers* and the reviewer got really negative, FSM knows why. She told me that we need IoC to make everything testable and "Helper" in the name is a code-smell. Sounds cargo-culting to me but I have only 6 years of experience so who I am to know.
* Now we have RequestExtensions and everything is apparently perfect.
There is some cargo culting there but it's mostly correct.
Helper is a code smell as it's a sign of "we don't know what the responsibility of this is or what to call it so we'll just chuck a load of shit in this file and call it a helper". The methods in should belong to something and live on that class, not in an external class.
RequestExtensions is more shit than the original solution. Extension methods are even worse! Shoot the reviewer.
This is a matter of taste not fact. In functional languages the style is compositional with static functions everywhere. It works well. The keeping data and methods together thing is one approach. Sometimes it's great. Sometimes unnecessary.
For example would you argue against string formatting helpers? Or would they need to be written to an interface and added to myriad DI bucket lists?
It's not that simple and it's not a fact. I'm an advanced user of functional languages as well and have written an entire scheme implementation before. I only semi-agree. That's slightly disingenuous representation of functional languages which have more than a few pitfalls. They certainly aren't the silver bullet and they really do not scale to the same height and complexity of the problem domain as the OO languages do due to the nature of the abstraction you describe. Nothing is particularly explicit. I'd rather take the compromises of OO over the maintenance problems of a functional language.
String formats are data so they would be stored as constants so that they are interned. They can be stored in a const class which is a static class with no methods i.e.:
sealed class StringFormats {
public const DateFormatX = @"...";
}
Also string formats for example tend to be owned by the respective objects so you can add overloads to the object to provide certain arbitrary representations. If the translation between an object and the string representation is complex, then you're really serializing it so that should be an abstracted concern.
Breaking the rules is good when appropriate. Problem is those rules are pretty amazingly good. I went through a weird phase of denial and ended up back where I started before I applied the aforementioned rules.
Duly noted! Although I'll try talking to her first, I'm sure there's more behind the decision :)
One of the methods that was inside takes a request, extracts the body and returns the parsed graph from the body. It's used by many controllers from many projects. I don't know where to put such a thing, hence the request extension.
Inject that into the caller via the container then you can mock the thing that calls it and just return a static Graph object, which you can't do with a simple extension method (which is why it sucks).
Extension methods are useful for only one reason: they trigger code completion for browsing what this object can do. Static methods suffer from FP code completion problems (you can’t complete easily in the first arg of a function/procedure).
I think I am not mistaken in saying extension methods, like lambda functions, were invented primarily for the use case of Linq. Even if they weren't, that's how Linq is implemented, so extension methods serve more than that "one purpose" if you don't insist on writing C# in the style of C# 2.0.
They came out at the same time, I’m sure there was some influence between them (Mads Tergesen would know better). However, all the functionality added in could have been done with static methods, just with more verbose syntax. LINQ query syntax could have been special cases. Anyways, I like what they came up with, it’s very versatile.
Why hate extension methods? Do you really want to write Enumerable.ToList(Enumerable.Select(Enumerable.Where(someList, e => e.someBool), e => new {a = e.x, b = e.y)) and so on?
Do you practise TDD? If you did a lot of this would make more sense to you. TDD is actually quite fun when you get the hang of it (less mental burden as you push all the 'intent' onto the computer).
I don't see why TDD requires ruling out static methods and insisting on hiding everything behind an interface. Static methods are straightforward to test, certainly more than a class with multiple dependencies which need to be mocked. Usually the complaint is about coupling when calling static methods but these can be wrapped in a delegate if required.
Simply because you can't mock the static dependency, therefore that method is now dependent on the static class and you don't have any control over it. This is problematic - what if at some point later another developer adds a database call into the static method to do some logging? Now your testing will dirty whatever database you're using, as well as run 10x slower - and yet the test will still pass and everyone will be none the wiser as to what happened.
If you start using a custom delegate solution, then your code is not consistent with everything else that uses DI, making it harder to understand. I can understand interfaces are annoying when navigating code, but the IDE still helps with that even if it is a few more button clicks, and the pros outweigh the cons.
> that method is now dependent on the static class and you don't have any control over it.
I don't see how you have any less control over it than any other code you wrote. If you don't want it to write log statements, then don't do that. Most static methods are small and pure so don't need to write log statements anyway.
> Now your testing will dirty whatever database you're using, as well as run 10x slower.
I've never used a logging framework that didn't allow you to configure where log statements were written, or give you control over the logging threshold for individual classes. However if your method is writing logs then presumably there is a reason, which is just as useful in the tests. If you mock it out then you're testing against different code to the one you will actually run against.
> If you start using a custom delegate solution, then your code is not consistent with everything else that uses DI.
Passing functions as arguments directly is 'DI', just without the need to configure that through an external container. Reducing the amount of interfaces (often with a single implementation) and external configuration makes navigating the code easier.
I think you missed my point, it's not about the logging framework, its about the fact you don't control an external dependency during testing. Unit tests are meant to be reproducible, meaning they are done under controlled conditions.
> Most static methods are small and pure
This is very assuming, tests are a way of being specific about your intent.
> its about the fact you don't control an external dependency during testing
If your code is structured using small static functions, you don't have any dependencies in the first place, just arguments you are passed and transform. You will probably create interfaces for external services you depend on, but you can avoid needing to mock them if you express the transform directly.
> This is very assuming
I'm not assuming anything, since I wrote the static method and I also decided to call it, presumably for the result it calculates. Your argument appears to be that static methods could contain bad code but that applies to all code you depend on.
You mean that the tests will depend on the thing being tested? What a crime!
> what if at some point later another developer adds a database call into the static method to do some logging?
Then you have a developer that does not grasp the idea of functions, and how they can help you improve your code. That's a call for education, not for changing your tests.
The point is that tests are omnipresent, people aren't. I've worked at places where all sorts of dumb code has got through because there is no automation in place to stop it, and everyone else is too busy to do code reviews.
In Java, you can use PowerMock to mock or spy anything, even private static final things. I consider it a smell (though excessive mocking even without powermock is its own smell), but it's immensely valuable to get code you can't change (or fear changing because of its complexity and lack of tests, or simply don't have time to change because the refactoring would take a whole sprint) to have some tests.
You don't need interfaces for everything in order to do DI. Interfaces should be used only for having multiple implementations or to break dependency loops.
Other than that I'm in agreement, static methods generally aren't a good idea. They can all too easily balloon into big chunks of imperative code with inner dependencies (static or not) at 10 indentation levels deep. Non static methods can too, but not as easily, and you have more options for fixes/workarounds in those cases anyway. The only place they really make sense is as part of a set of pure primitive data transforms, and ought to be small.
In C# we have Moq that can mock normal classes, though it requires adding 'virtual' to every method you want to override which is a code smell too. In Java everything being virtual by default I guess it doesn't matter. We like to always keep an interface around as it gets the developer used to working that way and keeps the code consistent. Visual Studio provides a quick shortcut to auto gen the interface too.
> Now your testing will dirty whatever database you're using, as well as run 10x slower
It sounds like a problem is a few layers higher. Why is there a live database in your unit testing environment? Why are working credentials configured? If they're unit test, not integration tests, all db operations should be DId / mocked / whatever. Any call that isn't should fail, not take longer time. Db interaction is for the integration tests.
You can even omit the db in the standard case if your language allows default keyword arguments. In almost every language, a method is just a fancy static call that takes extra arguments implicitly. (Closures are poor man's objects, objects are poor man's closures...)
Testing a database, or an external web service, is an integration test. They can be as simple as:
void TestCreateUser() {
var repo = new UsersRepository();
var mockUser = new User("John", "Smith");
repo.AddUser(mockUser); // db call
var addedUser = repo.GetUsers().Single(); // db call
Assert.StructureIsEqual(mockUser, addedUser);
}
For the Twitter web service, you might test that you successfully get a response, as you don't have control of what exactly comes back.
How is static code different from other noninjected code, like stuff in a method. Taken to the logical conclusion we'll have thousands or classes full of max 2 operations per method.
How many static classes are your methods using? And what is the problem with injecting this stuff at the top of the class instead? If you plan to write tests, you have to control your dependencies, and DI is the simpliest way to do that.
Problem is that the moment you start introducing delegates and crap like that is you're inventing a mechanism to work around your resistance to not using static methods rather than actually solving any problems.
There is no functional difference between a class with static methods and a class without, of which one instance is available to other classes.
Other than the fact that it isolates state, allows mocking and substitution and testing.
I disagree that delegates and higher-order function are 'crap' or in any way more complicated than introducing interfaces that are injected though a centralised container. You could just as easily turn that argument around and say mocking and an overuse of interfaces come from your resistance to using small static methods. In C# Linq is almost entirely based on static methods and delegates and that is not harder to test as a result.
Static methods usually don't rely on any hidden state at all. The example originally given was for a graph operation which could just take the input graph as an argument and return the result. When your code is composed of small independent functions you don't need mocking and substitution at all. In my experience most uses of mocks come from functions that do too much in the first place.
Yeah there is some cargo cult aversion towards statics.
Static methods with no side effect are wonderful, but static state is really bad and static methods which perform IO are horrible because they cannot be mocked in a unittest.
But some people miss this distinction and just say static methods are bad for testing.
The abstraction is consistent though, and familiarity is a good thing when navigating a codebase which has N amount of other devs pushing to it every day.
I practise TDD for peace of mind - if I add new functionality to existing code I can be 99.9% sure I haven't made any regressions. When a client's system goes down on a friday, I can 99.9% guarantee it wasn't my code that is at fault. If I have to work at the weekend to update a production server, I'm 99.9% sure it'll go smoothly as my tests say it will.
I can actually write entire features with appropriate test coverage from the ground up and they work first time and have close to zero defects in production.
It's amazing when you spend 5-6 days writing code that does nothing and at the last moment, everything slots together with a few integration tests and wham, feature done. Not talking trivial stuff here either; big integrations across several different providers/abstractions, bits of UI, the lot.
You see a lot of people arguing against this but I'm going to be honest, they churn out a lot of stuff that doesn't actually work.
> You see a lot of people arguing against this but I'm going to be honest, they churn out a lot of stuff that doesn't actually work.
My anecdata cancels out your anecdata. The TDD practitioners that I've met have, without exception, written code that worked fine for only the one case that they've tested. Example: They'd test a method for sending a message with the string "hello". Turns out the method didn't URL-encode the message before POST-ing it, and sending anything with a space was broken. They were confident and pushed the change.
Not saying you're wrong, just that TDD doesn't seem to work for everybody, and can even be a distraction.
That's an integration test really. The clients all have an abstraction around the http endpoints so nothing touches integration in unit tests. The advantage of this is you deal with transfer objects only in the code, no HTTP which would violate separation of concerns.
I use HttpMock myself in test cases which fires up an http server for personal projects. We use Wiremock commercially.
1) Write a test that runs the service and saves output to a file.
2) Mock out the call to just return the data from the file and validate results.
3) If you need variations on this data just modify the file/data (often as part of the test)
I usually leave number 1 in the code but disabled since it often relies on remote data that may not be stable. Having the test run more than once is not very beneficial but being able to run it later and see what exactly has changed is great.
In this case, what's the difference if you write the test before or after though? You would still be covered. I don't lean in either directions in this argument, just curious to understand.
The difference is night and day - writing tests first means you write 'testable code' from the beginning. Following the red, green, refactor mantra means that for every change to your code, you already have a failed test waiting to pass. The result is your test cases make a lot more sense and are of a superior quality.
To liken it to something you may be familiar with - when commenting your code, do you think it's better to add comments in as you write the code? Or add in the comments at a later date after the code is all written? I'm sure you immediately know which approach results in better quality commenting, and it's the same with TDD.
> To liken it to something you may be familiar with - when commenting your code, do you think it's better to add comments in as you write the code? Or add in the comments at a later date after the code is all written? I'm sure you immediately know which approach results in better quality commenting, and it's the same with TDD.
Not to take the analogy too far, but usually when writing a chunk of code I can keep it's behaviour in my head for a good amount of time and find it's best to add comments at the "let's clean this up for production" phase when you can take a step back and see what needs commented. If you comment as you go, you'll have to update your comments as the code changes and sometimes throw comments out which is a waste of time.
Likewise with tests, I'm not saying write them far into the future, but I think having to strictly stick to red/green/refactor is going to waste time. What's wrong with writing a small chunk of code then several tests when you're mostly happy with it? Or writing several tests at once then the code?
People just don't write comments or tests after, that's the problem. If you do then that's fine, but after trying both routes I actually find TDD to feel like less work - not having to wait on large build times and manually navigating the UI actually makes for a more fun experience. Instant feedback being the fun part. Additionally writing tests 'after' always feels like work to me and I end up hating it, especially when I didn't write it in a testable way to begin with.
> People just don't write comments or tests after, that's the problem.
Doesn't that get caught in code review anyway though? I find being forced to write tests first can be clunky and inefficient. Also, I've worked with people who insist on the "write the minimum thing that makes the test pass" mantra which I find really unnatural like you're programming with blinkers on. TDD takes the fun out of coding for me sometimes.
Generally I'd rather sketch out a chunk of the code to understand the problem space better, figure out the best abstractions, clean it up then write tests that target the parts that are most likely to have bugs or bugs that would have the biggest impact.
I find when you're writing tests first, you're being forced to write code without understanding the problem space yet and you don't have enough code yet to see the better abstractions. When you want to refactor, you've now got to refactor your tests as well which creates extra work which discourages you from refactoring. When the behaviour of the current chunk of code you're working on can still be kept in your head, I find the tests aren't helping all that much anyway so writing tests first can get in the way.
What you describe is the typical mindset against TDD, it's difficult to explain the benefits, and really you just have to experience them for yourself. Changing your mindset is difficult, I know, why change what works right? My only tip is to keep an open mind about it, as TDD benefits are often not apparent to begin with, they only come after a couple of days work or weeks or months later or even years later.
You find that you need to do less mental work, as your tests make the required abstractions apparent for you. 'the minimum thing that makes the test pass' ends up being the complete solution, with full test coverage. Any refactoring done is safe from regressions, because of your comprehensive test suite. And when other colleagues inevitably break your code, you already have a test lying in wait to catch them in the act.
> Any refactoring done is safe from regressions, because of your comprehensive test suite.
As much as I like the idea of TDD, I have a problem with this part. When some refactoring is needed, or the approach changes, it seems like you have two choices. One is to write the new version from scratch using TDD. This wastes extra time. The other is to refactor which breaks all the guarantees you got before. Since both the code and the tests are changing, you may lose the old coverage and gain extra functionality/bugs.
And unfortunately in my experience, the first version of the code rarely survives until the deployment.
I'm not sure what approach you've described here, but it isn't TDD. In the case of adding new features to existing code, as you are continually running tests you will know straight away which you have broken. At this point you would fix them so you get all green again before continuing. In this way you incrementally modify the codebase. Remember unit tests are quite simple 'Arrange, Act, Assert' code pieces, so refactoring them is not a time sink.
Also some refactorings are easier with tests, some are harder.
The kind @viraptor mentiones is the kind that spans more than one compoment. For example when you decide that a certain piece of logic was in the wrong place.
The kind of refactoring that becomes easier is when you don't need to change the (public) API of a component.
Take for example the bowling kata. If you want to support spares and strikes and you need extra bookkeeping, that's the easy kind of refactor where your tests will help you.
But if so far you have written your tests to support a single player and now you want to support two players who play frame by frame... Now you can throw away all the tests that affect more than the very first frame. (yes in the case of the bowling kata, you can design with multiple players in mind, but that's a lot harder in the real world when those requirements are not known yet)
> What you describe is the typical mindset against TDD, it's difficult to explain the benefits, and really you just have to experience them for yourself. Changing your mindset is difficult, I know, why change what works right? My only tip is to keep an open mind about it, as TDD benefits are often not apparent to begin with, they only come after a couple of days work or weeks or months later or even years later.
I've been forced to follow TDD for several years and also been given the same kind of comments to downplay any reasoned arguments against it which I find frustrating to be honest. I don't see why the benefits wouldn't be immediately apparent.
> You find that you need to do less mental work, as your tests make the required abstractions apparent for you. 'the minimum thing that makes the test pass' ends up being the complete solution, with full test coverage. Any refactoring done is safe from regressions, because of your comprehensive test suite. And when other colleagues inevitably break your code, you already have a test lying in wait to catch them in the act.
You can do all of the above by writing tests at the end and checking code coverage as well.
"Any refactoring done is safe from regressions, because of your comprehensive test suite. "
With the right tests this works great. I have also seen the opposite where a test suite was extensive and tested the last details of the code. Then the refactor needed more time to figure out what the tests are doing than the actual refactoring. As often, moderation is the key to success.
Unit tests should follow a simple 'Arrange, Act, Assert' structure and test one single thing, described in it's title. I agree anything too complicated starts to defeat the point, especially when we are mainly after a quick feedback loop.
> it's difficult to explain the benefits, and really you just have to experience them for yourself. Changing your mindset is difficult, I know, why change what works right? My only tip is to keep an open mind about it
Maybe writing code for exploration and production should be considered separate activities? The problem with these coding ideologies is that they assume there is only one type of programming, which is BS, the same as assuming a prototype is the same as a working product.
What's exploratory programming though? Unless you're writing something that's very similar to something you've written before and understand it well, most programming involves a lot of exploration.
Well, UX prototypes for one. In research, most projects never go into production, those that do do so without researcher code. Heck, even in a product team, if you are taking lots of technology risks in a project, you are going to want to work those out before production (and it isn’t uncommon to can the project because they can’t be worked out).
Not really. It makes you commit to an API upfront, this is the exact opposite of what exploratory programming should be (noncommittal, keep everything open).
No with TDD you don't need to go in with a structure in mind, the structures arise as you write more tests and get a proper understanding of what components you'll require. Red, green, refactor - each refactor brings you closer to the final design.
That's the mantra often quoted but it always makes me think of the famous Sudoku example from Ron Jeffries. Basically as a mantra it falls down if you don't understand the problem domain. It's popular because it works for the sort of simple plumbing that makes up a lot of programming work. This problem is particularly true for anything creative you're trying to express as the requirements are often extremely fuzzy and require a lot of iteration.
If you don't know how to solve a problem you actually need to do some research and possibly try a bunch of different approaches. Over encumbering yourself with specific production focused methodologies hurts. If you're doing something genuinely new this can be months of effort.
After the fact you should go back and rewrite the solution in a TDD manner if you think it benefits your specific context.
That really isn’t exploratory programming. The end result should be code that you throw away en masse (it should in no case reach production). Otherwise, production practices will seep in, you’ll become attached to your code and the design it represents, hindering progress on the real design.
When I was a UX prototyper, none of my code ever made it into production.
>I find when you're writing tests first, you're being forced to write code without understanding the problem space yet and you don't have enough code yet to see the better abstractions.
That's why it's better to start with the highest level tests first and then move down an abstraction level once you have a clearer understanding of what abstractions you will need.
Can you do that with TDD though? Why not just sketch the code out first before you start writing tests?
I find TDD proponents don't take into account that writing tests can actually be really time consuming and challenging, and when you've got a lot of code that is tests, refactoring your tests becomes very tedious.
>do you think it's better to add comments in as you write the code? Or add in the comments at a later date after the code is all written?
Define "all written". If we are talking about a new function - obviously you write you comment for it after the function ready to be commented on. And obviously you won't be commenting every string you put there, right?
Now, if we are talking about the whole new feature, that can consist of many functions and whatever - yeah, you usually comment your code in the process of writting the feature, rather than doing it at a later time, which will never come.
I also find when following red, green, refactor that you end up producing more targeted unit tests that are more expressive of the code you are testing.
Trying to write unit tests afterwards lands me with something that appears as more of an afterthought or add on. It doesn't have to be this way I suppose, but it is more prone to.
This might be because I am more used to the red, green, refactor method though.
I also practice TDD, but with a different 'T' - Type Driven Design. I find them much easier to reason about with types and safer (you can't compile your code if it doesn't pass the type check). Just model your data as ADT and pattern matching accordingly.
Of course, types alone can't represent every error cases out there (especially the one related to number or string), so I still write Unit Test for those cases. But the number of Unit Tests needed is much lower.
Visual tests are more general, and are more akin to putting up barriers on either side of a bowling lane so the bowling ball stays within it's lane (with room to move about still). For example when using Angular, you write 'Page Objects' that have methods such as .getTitle(), .clickListItem(3) and so on, and can then write assertions to make sure the UI changes as expected by inspecting properties [1].
I usually find I build a general page object first ('this text is somewhere on the page'), then write the UI, then make the test more specific if I can after (but it's an art, as too specific and you risk creating too many false negatives when you make UI changes).
(Also as you are interacting with the UI, these would be known as integration tests.)
I don't think you can unit test GUIs, since by their nature all tests end up being integration tests. It's easy if you assign non-css (i.e. use a data-* attribute for identification instead of id or class since you want to keep those variable for stylesheet refactors) identifiers and just hard code the assumptions into the tests, like "when x is clicked y should be visible", or "when I enter 'foo' into the text field, the preview label should contain 'foo'". Ideally your assumptions about GUI functionality shouldn't change much throughout the lifetime of the project, and if you use static identifiers your tests should hold up during extensive refactoring.
To a certain degree you can unit test GUIs with tools like Ranorex or Selenium. The question is how much setup you need to get the GUI on the screen with the right data.
You can, but usually with lots of effort and cannot test UX and design requirements anyway, which is why I tend to make this question about full TDD based processes.
I saw an enjoyable talk recently about snapshot testing. I don't know too much about testing generally but it seems like it could be relevant: https://facebook.github.io/jest/docs/en/snapshot-testing.htm... is the general idea but it doesn't have to be confined to jest/react
Past my edit window, but I want to add this - I feel that I introduced some confusion by missing one magic word in one special place. The first sentence of the last paragraph should be:
That said, if you go for functional style in OOP, i.e. shoving as much as you can into stateless static helper functions and most of the rest into dumb private stateless functions, (...)
Of course I do not mean you should abandon objects where there is a strong connection between a set of data items and operations that work on them, or where polymorphism is a right abstraction. But from my experience, quite a lot of code is made through transformations applied on simple data, and when you write that kind of code in a functional style (whether as static methods grouped in helper classes, or private methods within an implementation of your class), both quality and testability rises in lockstep. And my point is that quite a lot of code can be written this way even in an OOP project.
>That said, if you go for functional style in OOP, i.e. shoving as much as you can into static helper functions and most of the rest into dumb private stateless functions, you suddenly gain both a clean architecture and lots of test points to use in unit tests. So you can have testable code, but you have to chill out with the OOP thing a bit.
Wow, this is exactely totally opposite of how one can achieve testability in OOP! For more details I recommend excellent Misko Hevery's article "Static Methods are Death to Testability" [1]. Also, I'd argue that "functional style in OOP" is an oxymoron - you're either OO or something else (functional, imperative...)
> The basic issue with static methods is they are procedural code.
So is any object-oriented code. OOP is a subparadigm of procedural programming.
> Unit-testing needs seams, seams is where we prevent the execution of normal code path and is how we achieve isolation of the class under test. seams work through polymorphism, we override/implement class/interface and than wire the class under test differently in order to take control of the execution flow. With static methods there is nothing to override.
Why did it not occur to him that the function boundary is the "seam" he's trying to find?
I mean, `method(a, b)` is equivalent (as in: equally expressive, and usually implemented in the same way) as `a.method(b)`. Therefore, any problems with one case equally apply to the other case. If his problem is that `method(a, b)` may call other, non-mockable functions, then that criticism equally applies to `a.method(b)`.
(As I'm writing this, it occurs to me that the author may be suffering from the "OOP = Java" delusion.)
The OOP = Java trap is all too common, but the converse is also a trap: just because you've written OOP code in a different environment doesn't mean that pattern will work in Java.
Go with what the ecosystem supports, and you'll find your tooling helps you a lot more than if you fight against it by trying to force non-idiomatic structures. Your colleagues will appreciate it, too.
He probably meant no-side-effect static functions. I myself find using these a lot. For common CRUD web apps, you have Spring doing most of the stuff for you and you simply need to write stateless methods. However, for not-common requirements, you might need to use classes and OOP patterns to implement a complex logic.
It seems the encouraged method is IoC these days, and that's just dreadful. IoC/Dependency resolution all over make it insanely hard to reason about code without running circles through the codebase.
For me, IoC seems invented almost entirely to make up for how difficult testing can be in particular languages. Which, sure, making up for shortcomings as good, but the necessity to use IoC for it feels bad.
I've used it a fair deal. I've found I prefer languages that don't require IoC to make code testable.
I agree that it's one of the sanest options when it's required, I just think that language design should incorporate testing ergonomics from the start.
> unnecessary abstraction and boilerplate patterns
That means you didn't actually change the code. It means you added unnecessary abstractions around your code in order not to change it.
Unit tests guide you towards simplicity. In my experience, the only times they haven't done that is when I have made some assumptions about what the code should be and not allowed the tests to drive me towards that simplicity.
That said, if you go for functional style in OOP, i.e.
shoving as much as you can into static helper functions and
most of the rest into dumb private stateless functions,
We had this at a company I worked at a while back - dozens of modules with nothing but static functions that all took a first argument of the same type. If only there was some kind of METHOD for declaring a whole bunch of functions that operated on the same data...
Until you get into polymorphism etc. this is just a style thing.
method(a,b) is equivalent to a.method(b) and exactly as much typing. You do save manually typing the extra part of the definition but 'eh'. A few languages treat these interchangeably.
The biggest problem I see with people advocating for tests and employing TDD is that they do change how they write code to accommodate tests. This leads to inclusion of lots of unnecessary abstraction and boilerplate patterns that make code less readable and more bug-prone. OO world has spawned numerous non-solutions to turn your code inside-out so that it's easier to mock things, at the expense of code quality itself.
That said, if you go for functional style in OOP, i.e. shoving as much as you can into static helper functions and most of the rest into dumb private stateless functions, you suddenly gain both a clean architecture and lots of test points to use in unit tests. So you can have testable code, but you have to chill out with the OOP thing a bit.