6 ms·
Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code?
by agnosias 6y ago
Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code?
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?
- Xunjin 6y agoI wish to know this as well. And what about functional programming languages?!
- garethrowlands 6y ago> Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code? 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.
- Xunjin 6y agoIt does thank you, learnt a bit about partial application when dabbled with Haskell, tho never never really got close to Monad stuff (io I guess?!). Now I'm trying to learn more about Rust also I identified references about "Working effectively with legacy code". 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 :|
- garethrowlands 6y agoHonestly, I think "Working Effectively with Legacy Code" is the main work in this field. 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.
- robertknight 6y agoAn option in some languages is to simply create an alternate mock/fake version of the actual class or function that is depended upon and monkey-patch the code under test to use it for the duration of the test. This is commonly done in Python ( see `unittest.mock.patch`) and JavaScript (eg. with Jest mocks) for example. 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.
- h4kor 6y agoThis was one of the most important lessons in my career. If you want to test your code thoroughly you needs these techniques.
- tonyedgecombe 6y agoWhereas one of the most important lessons of my career is that you don’t have to unit test everything.
- stepbeek 6y agoI’ve generally understood the criticism of DI to be levied at codebases where it was applied to literally every class. I’m not the author, but I don’t imagine that relying on interfaces to abstract between layers of an application or to avoid a concrete dependency on something like an external api to be what he was arguing against.
- aszen 6y agoThere are two ways of abstracting out an external api, one is via an interface with a set of methods this is well understood but there is another option as well that is to focus on the core data types your application needs. Create those types and then revolve your whole application logic around them. Finally create decoders that instantiate those types from different sources.
- stepbeek 6y agoThis sounds interesting. Can you link to any examples?
- aszen 6y agoGo through https://fsharpforfunandprofit.com/fppatterns/ https://fsharpforfunandprofit.com/fppatterns/ And https://fsharpforfunandprofit.com/posts/13-ways-of-looking-at-a-turtle/ 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
- mrloba 6y agoMove your logic to pure functions. Use classes for plumbing. Now you can unit test the logic, and integration test the plumbing. You can easily test with a real database using docker for example. Note that you'll still use DI in this case, but you'll have far fewer classes and therefore fewer dependencies as well.
- bandyaboot 6y agoThat was one of the eyebrow-raising things the author touched on, “automated testing theatre”. Really? Unit testing is pointless...is that now a thing? Is there another industry that is constantly changing its mind or debating every aspect of itself quite like the software development industry?
- jdlshore 6y agoThe author is the creator of BDD. I suspect you’re misinterpreting his words. You can complain about “automate testing theatre” without being against automated testing.
- bandyaboot 6y agoThanks. I’ll read some of his stuff on the subject.
- herval 6y agoThe natural segue for this sort of contrarian “I don’t need your patterns” stuff is logically “I don’t need unit tests either”, soooo pretty sure the answer there is “you don’t need tests”
- garethrowlands 6y agoWell, he invented BDD, which very much builds on TDD, so, no, that's not the direction he's going.
- herval 6y agoWow that makes it even worse. I had no idea, apologies.
- jnwatson 6y agoYou pick a language that doesn’t require you to bend your architecture around testing. 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.
- aszen 6y agoFirstly if the dependency isn't doing any io, you can test your code as a whole along with its dependency. No need to mock. 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.
- garethrowlands 6y agoSome of this is covered in "Functional Core, Imperative Shell", https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct... Haskell programs tend to have this structure because pure functions aren't allowed to call impure functions.
- globular-toast 6y ago> Firstly if the dependency isn't doing any io, you can test your code as a whole along with its dependency. No need to mock. 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 https://www.youtube.com/watch?v=EZ05e7EMOLM
- 6y ago
- jdlshore 6y agoYou can separate your code into “logic” which has no external dependencies (just logic, eg a domain model) and “infrastructure” which does. Infrastructure still requires DI but Logic doesn’t. 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.
- user_agent 6y ago'How do you unit test your code without mocks?' 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.
- ItsMonkk 6y agoMark Seemann's blog I've been reading for the past couple years answers this question. In particular, his posts on Hexagonal Architecture[0] and Dependency Rejection[1], but there are many more articles that cover this and I highly endorse reading. 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-adapters-its-all-the-same/ https://blog.ploeh.dk/2013/12/03/layers-onions-ports-adapter... [1]: https://blog.ploeh.dk/2017/02/02/dependency-rejection/ https://blog.ploeh.dk/2017/02/02/dependency-rejection/
- snidane 6y agoYou are doing interface/function level testing and calling it unit testing. 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 https://youtu.be/EZ05e7EMOLM
- geofft 6y agoThe good idea of unit testing is repeatable, reliable tests, ideally those that can be run as part of CI before every merge and also on your end-user machine. "Unit" testing imposes an architectural demand on how you do this. 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.
- madhadron 6y agoPeople have already pointed out using pure functions for the heavy lifting and then making the IO as simple as possible, but another issue is tooling. If you have something like Standard ML's module system or COM style components, you define a functor/interface/contract that your code depends on, and then implement a test form and a production form. This is basically what dependency injection is doing, but working around the fact that modular programming didn't really catch on.
- drblast 6y agoMost of the time I've seen dependency inversion "so we can test" the code is littered with hundreds of single-implementation interfaces and no mocks, or stub mocks that are written just to pass the tests. It doesn't actually test anything. 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.