1) Join a project and fume about how they didn't write nice, clean, "testable" code.
2) Build some sort of integration testing framework that also mocks out REST API calls relatively cheaply (e.g. using recording/playback against a live system).
3) Realize that I can now refactor the code such that it is nice, clean and more easily "unit testable".
4) Realize that with an effective and easy to use, fast integration testing framework the lack of dependency inversion doesn't bother me nearly as much as it used to.
5) Wonder if dependency inversion is actually more of a hack to deal with the sheer utter crappiness of unit tests... and whether making vast architectural changes to code in order to accommodate the sheer ineffectiveness of the current state of test tooling is really the height of wisdom after all.
In a number of these projects there were noises about rewriting the whole damn thing in a different language (usually because management wanted to consolidate hiring around fewer languages), and it occurred to me that it would actually be kind of great to write tests where the entire implementation including the language itself could be swapped out without really changing the tests.
The creator of BDD gave this talk a few years ago, which resonated with me: https://www.youtube.com/watch?v=4Y0tOi7QWqM
> it occurred to me that it would actually be kind of great to write tests where the entire implementation including the language itself could be swapped out without really changing the tests.
This is exactly what I've been doing and advocating for for a while. Don't mock things like databases at all in your code. If you want to use a mock, use a "first class" mock like https://cloud.google.com/bigtable/docs/emulator.
The other thing is that TDD was born in a world where containers aren't the norm. All the pain of higher level testing really comes up from setting up an appropriate environment. If you're already using Docker or k8s, that pain is pretty much 0.
I think this basically already exists with "Cucumber" and behavior-driven development. The test definition is specified in a natural-language-looking DSL which supports a number of underlying languages. When you swapped out the system you would have to implement the step definitions in the new language, but the tests would remain valid.
- Now it's harder to navigate my codebase because I introduced an extra layer of indirection everywhere only for the sake of testing.
- If there is some difference between how the actual protocol client and actual upstream behave and how my fake interface behaves in test, my tests will be wrong, and I will find out when I push to prod. If my tests exercised the actual code that speaks the actual protocol to a fake upstream, I may have caught the issue in the tests.
- If I ever want to replace the process entirely with a completely different one, I must now throw away my tests. If my tests tested the end-to-end behavior of the system, I would be able to keep my tests.
2) For each unit test setup an independent mock of the interface for your subject-under-test. You should not create catch-all mocks for all your interfaces, unless it's to be used for offline work.
3) If you replace the process, as in the business process, you need to redo the tests as well yes. If you replace the implementation, you must throw away your "white-box" tests but should be able to keep your "black-box" tests.
2) Right. I agree.
3) It looks like your original post in this thread says "Do not make black box tests. Make white box tests"
3) All right. That was not my intend. I think they both do different stuff, some projects need both, some only need one or the other.
I.e: If you have a "stack" like this:
- Application
- MainService
- ApiService
- HttpClient
What you might think of doing is replacing the ApiService with a mock, such that you can test your MainService's interactions with it.
You can do that, sure, and it's useful - but as you rightly point out now you can't find any bugs in your ApiService.
Instead, mock out the HttpClient (or if it's not Http, mock out the Socket). Typically because these kinds of things are at the bottom of the stack, they have very simple interfaces and are really easy to mock, and because the interfaces are so simple the mocks don't introduce much - if any - damage to the rest of your code around them.
There are significant advantages to doing it this way too, over using an external mocked API.
- Way faster
- More reliable
- Single codebase (if the mock API is some external NodeJS thing now you have to wrangle that too)
- You have perfect control over timing. For example, if you have a bug in your ApiService which happens when two packets get interleaved in exactly the wrong way, you can reproduce this easily. If you had a real HttpClient talking to an external process it's nigh impossible.
Good luck!
It does this by first making the actual request and then storing that record in a plain YAML file (which you can alternatively generate yourself, by hand or from an openapi spec)
Have a look at "Ports in Other Languages" in their README to find the VCR port for your language, framework or stack.
If the only reason for abstraction is testing, then mocking an interface seems like a more valid choice to me.
Then you're likely in the wrong business, since most of what programming is is dealing with the various facets of one abstraction or another.
I just prefer to abstract when I have more reasons that just testing. That can be done using mocking techniques.
https://docs.spring.io/spring/docs/2.5.x/javadoc-api/org/spr...
How is that different then defining an API and then having stub methods on the front returning pretend data?
Both the API and the Mock implementation will require the same interface.