4 ms·
Write everything as functions. Your dependencies are also functions, which are passed in as arguments to your functions. To unit test your logic, pass a depend
by asimpletune 3y ago
Write everything as functions. Your dependencies are also functions, which are passed in as arguments to your functions.
To unit test your logic, pass a dependency that does the situation you’re looking to test, Eg () => panic. With this alone you can catch most of your logic issues.
For integration tests pass the real things in, but only test the interfaces you publicly expose. With this you can discover when a dependency has broken/changed out of band.
Last thing is write your functions as you write your tests. This way you can discover requirements while it’s still easy to change your code.
Every once in a while it makes sense to refactor. The process of writing your tests will tell you when, and the tests themselves will tell you that your new code is equivalent.
This is more or less what I’ve been doing for years now and it works. Very few bugs get past this. Code is easy to read and easy to change. Abstraction and documentation happen progressively too. Boss and team mates are also happy.
Maybe the one source of friction from this approach has been the type who scream “test against reality”. That and jira.
(The one thing I forgot to mention is to model data like records as much as possible. The data is really the key, everything else above is easy.)
- peheje 3y agoWhat language are you using? I'm using C# at work, but dabble in F# in my free time. I try to find a F# alternative to the C# way, seems like the simplest alternative to dependency injection through containers are just function passing in F#, like you are mentioning. One thing that I would like to know, is how do you do the equivalent of "registering dependencies" like you would using a DI-container. Where do you setup all the (partially applied) "real" functions and all the "fake/mock" functions etc. I guess it's just a matter of structuring it in a nice way.
- pharmakom 3y agoIf using F# the easiest way is usually to make an interface for the impure stuff and write a mock implementation of that. Pass the interface in as a parameter to your functions. Keep that interface as small as possible.
- asimpletune 3y agoThe above related mostly to work I did in Scala, but I am writing mostly Rust now. Each language has a way of doing things so you have to adapt what I said to how it makes sense for you. Still, the process is the same in spirit. > how do you do the equivalent of “registering dependencies” In what I was describing earlier, dependency injection is literally just calling a function. The dependencies are arguments to a function, and you “inject” them by supplying them to the function. The “registering” is just based on your function signature. That is already a contract. Dependencies are functions too. > where do ou setup all the functions Usually one of two places: tests or your app’s entry point. In tests you just setup your function wherever it’s needed. Like I said if you need to test what happens when a dependency of yours does something, then just write a function that returns what you are looking to test. For your app, this same thing is in your app’s entry point, like main. So you read from config and build your dependencies, and then at the end you ultimately pass them to a function. At the end of the day this is about inversion of control, but it’s something that makes more sense if you invent it on your own from scratch before trying to understand what that means through more complicated abstractions.
- necovek 3y agoI do a slight modification of the approach: I pass everything in, but if you need related functions passed in, it makes sense to pass them in as a service object containing those functions. Those service objects really have no side effects (constructor basically simply keeps context in), allowing for smaller function signatures, especially for, by nature, side-effect stuff (logging, caching...). Basically, saves on passing both "get_from_cache" and "set_cache" functions in as you pass CacheService instance in. But the premise is the same: keep code functional, allowing for easy testing and clear implementation. The one thing I disagree is that this does not allow testing against the real thing: you simply have a test that confirms dependency and simulated fake (function or service) have the same basic behaviour your code expects. If the real thing works using an external service, you can decide if you want to test that as a system test (I run those ocassionally) or with mocks.