Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.
How do you do it?
Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.
How do you do it?
More interesting is if your code relies on the outside world, then instead of abstracting out the connection with the outside world abstract out your business logic and test it separately.
So instead of a database repository being injected into your domain services, make your services rely on pure domain objects which could come from any where be it tests or the database.
Make a thin outer shell which feeds data into your domain logic from the outside world and test that via integration tests if necessary.
I'll admit I don't have the full picture here, but I have used this technique to good effect. The core idea is don't embed your infrastructure code deep inside your architecture instead move it to the very top.
Haskell programs tend to have this structure because pure functions aren't allowed to call impure functions.
This the thing. People have a tendency to overuse mocks. The point of automated testing (whether it's unit tests or something else), is to enable refactoring. That's really the only reason. Code that you don't touch doesn't suddenly change its behaviour one day.
In a previous jobs the developers started to go crazy with mocking and it reached a kind of singularity where essentially if a function called anything, that thing was mocked. It definitely tests each function in complete isolation, but what's the point? It makes refactoring impossible, which is the entire point of the tests in the first place!
This excellent talk completely changed the way I approached testing. Every developer who writes tests needs to watch this now! https://www.youtube.com/watch?v=EZ05e7EMOLM
> Code that you don't touch doesn't suddenly change its behaviour one day.
This can and does happen all the time, when the platforms and abstractions your code builds on change underneath you. This is why a compelling environment / dependency management story is so important. "Code rot" is real. =P
That's what industry converged on, that a function/method = a unit, but it apparently used to be that a module meant to be a unit.
It can be that both interfaces and modules can be considered a "bag of functions".
Seems like a lot of confusion about unit testing, mocking and DI stems from this historical shift.
I believe that interface/method level testing is too granular and mostly results in overfitted tests. Testing implementation, not behaviour. Which can be useful for some algorithm package for example, but probably not so much applicable to typical business logic code.
TDD: where did it all go wrong. https://youtu.be/EZ05e7EMOLM
The end result is the same as if you'd created an interface or abstraction with two implementations (real and fake/mock), but you skip the part where a separate interface is defined.
The upside to this is that the code is more straightforward to follow. The downside is that the code is exposed/potentially coupled to the full API of the real implementation, as opposed to some interface exposing only what is needed.
I strongly recommend reading "Unit Testing: Principles, Practices, and Patterns" by Vladimir Khorikov.
That's going to answer your question and will unveil a whole world of testing without mocking (not that it's better or worse than with mocks). It might be a single best book about unit testing in general.
What you actually want is fast and self-contained integration tests. Have an API endpoint that returns some filtered data from the DB? Spawn a DB server locally, load some fixtures into it (e.g., phrased in the form of a migration), start up your application server, and curl it and see if the data is filtered. Then shut it down. In most stacks you can do all of this in a fraction of a second.
The reason most shops love unit tests is that it saves them from having to figure out how to run those integration tests. It's easier from a Conway's Law perspective to use your language's built-in testing facilities to test the function you're writing, let the ops team figure out how one starts up a database server, and let the frontend team figure out how one makes HTTP requests to your application. But you will deliver better software if you spend a bit of time figuring out all those things and stick them in a tiny shell script.
Some programs are nothing but Infrastructure (DB backed microservices) but some have complex Logic (complex problem domains). In the latter case the most important parts of the system can be tested without DI.
You still need DI but not pervasively.
In the case where there are mocks, the unit tests are almost always literally testing "can my programming language call a function and return a result" or "can we store data in a database if the database works?" They are functional tests disguised as unit tests, and are testing only non-production fake functionality.
Write functional code instead. Liberally use the compiler and the type system to make mistakes impossible instead of unit testing.
Unit tests are for when you can't express something in a way where the compiler and type system will save you from errors.
Everything else is a functional or integration test and should be testing the production system, not a mockup of the production system that works differently.
Basically, write Haskell programs in whatever language you're working in. Use unit tests as a backstop for when the type system isn't as good as Haskell's.
To summarize, you want to set up your code into two distinct categories, data and pure functions. Your data is at the top level of your code, so if you need external data that needs to be at the top level of your module. You then feed that data through a pipeline of pure functions that return with some sort of answer back to your top level module that can then pass into some other function(or through a quick lambda) that does the actual transformation at the top level. The key is that a function can either be a pure function or it can be an impure function. Never both.
As an example, let's say you've got code that has to read from a couple text files. Currently your code does this through a couple nested loops, with the loops passing in what folder it is in and what its file name is, but your code is filtering through some that you don't want to read.
To re-envision this code, you would create a pure function that comes back to the top of your module of which files to read. Then that top module would read those files, and pass them to some other function that does transformations on the result.
You now don't have to create mocks, just create data and pass that into your pipeline and then check if it gets the expected result. One of the interesting side effects of doing this, is at a certain point you stop unit testing for correctness(as the code is so easy to reason about written in this style), and start unit testing primarily for documentation.
[0]: https://blog.ploeh.dk/2013/12/03/layers-onions-ports-adapter...
This is a classic example of software-pattern-being-language-defect.
It is trivial to mock out upstream or downstream dependencies in Python, without any changes to the code under test.
This is basically what dependency injection is doing, but working around the fact that modular programming didn't really catch on.
Finally create decoders that instantiate those types from different sources.
https://fsharpforfunandprofit.com/fppatterns/
And
https://fsharpforfunandprofit.com/posts/13-ways-of-looking-a...
I'm still learning on how to apply these ideas in my own work (which doesn't use f sharp or functional programming) but it's a rich source of ideas
You can complain about “automate testing theatre” without being against automated testing.
The author isn't actually demanding you change your tests (unless they are unnecessarily complicated).
> Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.
Unit tests require 'seams' to divide the tested code into units but those seams don't have to be interfaces. Some languages don't have interfaces at all - Ruby, say - and they unit test just fine. The pattern of every Foo having a corresponding IFoo is a bad thing - you should replace IFoo with smaller role interfaces. Having an IFoo if it isn't necessary to test Foo and there's only one implementation is indeed over engineering.
> I wish to know this as well. And what about functional programming languages?!
Unit testing's much the same in functional languages, though it tends to be easier because pure functions are easier to test. But how code gets its dependencies tends to differ a bit. It may help to think of dependency injection as parameterisation. Or, perhaps, parameterisation where you don't use the parameter immediately.
Suppose I want to test a function foo that depends on another function bar:
fun foo() {
return bar()+1
}
Well with objects and interfaces, I can do this: interface IBar {
bar()
}
class Bar(): IBar {
fun bar() { return 2 }
}
class Foo(val bar: IBar) {
fun foo() {
return bar()+1
}
}
And then I can make a test double for Bar in my test, and when I invoke foo, then it will call the bar of my test double.Now, of course, in this example, I could test with the real Bar and I don't need a test double. That's partly because the example is simple but also because foo and bar are pure functions. But let's ignore that and see how we can do the same in a functional language.
fun foo(n: Int) {
return bar()+n
}
The question is, how can we control the value of bar in our tests?The simplest answer is that, since bar is varying (between test and prod), then bar is a parameter:
fun foo(bar: ()->Int, n: Int) {
return bar()+n
}
Now the reason we don't do this in OOP languages is that we won't have bar at the call site. That is the production code calls foo like this: foo(n)
and not like this: foo(myTestBar, n)
That's why our test code looks like this: val myFoo = Foo(myTestBar)
assert(myFoo.foo(3)).isEqualTo(something)
though, if foo takes its dependency as a parameter, the test could just look like this: assert(foo(myTestBar, 3)).isEqualTo(something)
But the prod code won't look like that, it only passes n. So we need to pass bar before that. That's why we pass bar to the constructor in the OOP version.OOP languages generally expect you to pass all the parameters to a function when you call it but some functional languages don't require this. That is, you can pass bar on its own - a mechanism known as 'partial application'.
You'd use it like this:
// Do this when 'wiring up' the application
val myFoo = foo(myTestBar)
// now I only need to pass n
myFoo(n)
If your language doesn't provide partial application, then you can do the same with a function that captures the dependency: // Do this when 'wiring up' the application
val myFoo = {n-> foo(myTestBar, n)}
// now I only need to pass n
myFoo(n)
Now, notice that, since bar is just a function (of type ()->Int), I didn't need to create a separate interface IBar. For this purpose, functions "just work". They are equivalent to interfaces containing a single function (Java's SAM recognises this, if that's familiar to you).Notice also that an interface of a single function is as small as it can be. Often, we'll see a class Bar with, say, ten methods, and a corresponding IBar also with ten methods.
SOLID's Interface Segregation Principle says to prefer small 'role' interfaces over obese interfaces such as the ten-methods IBar. And, of course, if you're using functions, that happens naturally.
Hope this helps.
My point being, do you have more references for studying about tests?! As a NodeJs and Python developer today (with almost no tests) it's really hard work with these legacy code :|
If you can pair with someone who habitually works TDD, do so. Sounds like getting experience in a team with effective modern practices (see "Accelerate" by Forsgren et al) could change your life for the better.
If you want to test your code thoroughly you needs these techniques.