If you can, set up a real DB, cache, whatever, and run your services in tests with real instances of their dependencies. Obviously this falls through for external APIs and things that are very expensive, but your savings in "But the tests passed" confusion time will be huge.
*I still think unit tests are great for tricky logic, but a lot of coding is just gluing together different systems where unit tests don't have a lot of bang for their buck.
If you want to test how your code behaves when getting/setting data from a database, you fundamentally cannot mock the database. If you want to test how your code forms arguments to send to a dependency, you probably can, as long as you do both. If you're just testing that your args are correct without actually using them, you'll never really know.
A specific example stands out in my mind when I watched a TDD-minded developer suggest that their code was complete because they injected a mocked version of DynamoDB into a service and then verified their tests ran. They committed and pushed without actually testing against DynamoDB. When tasked with _actually running it_, they found that there was a fundamental flaw in the way that developer perceived Dynamo's behavior, which obviously meant their mock was totally useless.
If you want to test that you're pulling the right ID off an object to send to a database you can mock. If you want to test how your code actually behaves with a database, including failure scenarios and invalid arguments, you cannot.
As usual, solutions are much more nuanced than I previously stated. I've moved away from mocks, not banished them entirely.
But mocks could be useful for testing that your code correctly handles errors that are returned by an external component.
This is what makes mocks of limited utility. In the end, they don’t prove the code works and they are expensive to create and maintain.
I also refuse to measure test coverage in my codebases. "How frequently do bugs show up in production" and "how frequently are bugs fixed without adding tests" are metrics I find valuable but are underrepresented in the testing space. If bugs don't make it to prod very often, and when they do they are fixed, your testing strategy is probably sufficient. There's no reason to write thousands of null checks and formatting validators if you don't need to. Tests require as much or more maintenance as code. There's no reason to write more of them than you need.
I too generally only write unit tests for tricky pieces of code, especially when it's dealing with concurrency which tends to be difficult to test deterministically at the API level. There are other good reasons to write unit tests, such as execution speed, complexity of setting up a testing environment, faster development feedback loops, etc.
Maybe that'll help you convince your managers to invest in the kinds of tests that make sense for your product.
You should shoot for both, actually. If your tests require a live DB to run, they can't be automated (or they can be automated, but failures won't tell you much useful).
There's a non-unit-test value of having good code coverage, too: being able to run any function independent of the rest of the app, with arbitrary inputs. When you have a problem that's difficult to reproduce in a controlled environment, it's really useful to be able to run a function that's normally ten function calls deep all by itself over and over again while trying to isolate a problem. If you have to run the entire app to run any of it, that becomes impossible.
Can you define a scenario in which hitting a real DB wouldn't present useful information while a mocked DB would?
If you're relying on a shared database you're already not really testing effectively. I don't really think this is a scenario to worry about (meaning, if you're in this scenario, you have bigger things to worry about).
> if the DB is down.
An interesting suggestion.
The use case of mocks/stubs/etc for me is “this thing is expensive to execute or external and can’t be validated directly”. In the former case, validating the inputs to the expensive interface is buttressed by thorough and verifiable testing of the thing it calls into. In the latter case, you provide an interface that behaves according to your understanding of the external service without requiring its availability.
In pure unit testing, the “external” boundary is hungry and consumes as much interaction between code components as possible. In behavior testing, you extend end user validation as far as is practical or possible.
The benefit of that is you can verifiably satisfy user needs up to the limits of your testing/testing environment, in user language. Wherever you need to silo who the “user” is (eg service/api boundaries), you can do so. But your tests are designed to validate user assumptions first, and fussy technical details after.
Sure, you might want to say that I should just use IDE which will show me all public methods in a GUI tree control. But I don't like it, I like code. May be some feature which completely hides anything in the class except public method declarations could substitute, but I did not see that feature yet.
Honestly I don't know what's wrong with an interface, even if there's a single implementation. Idea and Eclipse allows you to jump to implementation with a single click if that's what you're looking for. JIT will replace virtual method calls with direct ones.
I once had to work on a code base where every SQL query was inside a DaoImpl (ok) which was an implementation of a Dao interface (questionable) which is then called by a BoImpl (why on earth?) which is an implementation of a Bo interface. By your logic it would be sensible to add even more interfaces even though the intermediate classes do nothing except pass a function call to the next layer. Want to add one parameter to an SQL query? Well vbezehnar said it's fine to modify 6 or 8 files to accomplish such a trivial task.
The irony is that the whole code base was garbage and hardly readable and the biggest culprits weren't the overuse of patterns (they were just the cherry on top) but rather the fact that a random bundle of libraries has been misused for purposes that they were not designed for. My personal favorite is keeping hibernate objects beyond the lifetime of a hibernate session and the hibernate session was configured to live as long as the http request. Combine this with a web framework that completely abstracts away http requests and it means you can never use hibernate correctly because you never know when the session has expired.
It’s hard (okay it’s not hard, it takes study and practice) to know what the “right” abstractions are, and it doesn’t produce IDE solutions (unless you’re working from a strong theoretical foundation).
It certainly doesn’t help when the type system doesn’t help. Lots of great abstractions are available when you can derive and infer meaningful types and derivatives at the end interface without a lot of fuss. It doesn’t just improve DX for library and product devs, it also improves design instincts.
A more functional style often fares better in my experience. Functions just accept “some data” and return “some other data”. Even without Haskelly algebraic data types, “some data” in a function that isn’t explicitly tied to a concrete type is a good prompt to think about the kinds of things that are common or disparate about the data and how to handle them.
It doesn’t necessarily lead to fantastic patterns. Most functional-style-but-idiomatic TS is not as well designed as equivalent code in an ML, error handling can be hell. But at least in my experience it has better outcomes than the OOP equivalent.
There's still a milion ways to do the same thing, and there are still smartass people going out of their way to produce code that makes people notice how unique they are.
All at the expense of wondering what the memory and performance will be.
I'm sold on "pure PHP" but you can get a hell of a lot done with a few simple libraries and avoiding all the bullshit pushing you into classes.
Typescript is quickly falling down this rabbit hole as well.
The balance I do like to strike is to use interfaces for any classes that interact with the outside world, and then have those faked out.
That way, I can use them to provide data to smaller units, or record that business logic recorded the data I expected.
Obviously this doesn't fix if the DB interactions themselves are correct, but it's a start.
I vaguely remember language support for something like this in the SatherK dialect of Sather. Sather itself was inspired by Eiffel. One-interface-per-class actually is no problem if it's well supported in the language, by simply adding a single special character to the concrete class name.
It worked something like putting some sigil (dollar sign, IIRC) in front of a concrete class name meant that the code was referring to the public interface exposed by the class. (I could have gotten it backward in that maybe you needed the sigil in function signatures/field declarations if you wanted to force the concrete implementation instead of the interface. I think it evolved from the normal Sather dialect, where interface declarations looked pretty much like class declarations except that the name began with a sigil. If it hadn't evolved from standard Sather, I suspect they would have required the sigil if you really meant to intentionally restrict your code to the concrete implementation.)
Java could have headed off a lot of premature abstraction by making it easier to later replace a concrete implementation with an interface. Instead of making "new" an operator, Java should have made "new" a static method returning an instance of the class, with a bit of compiler flow analysis to allocate an object the first time you referenced "this" within "new". The chief advantage is that it would have minimized the amount of code change necessary to replace a concrete class with an interface if needed. If new is a static method, then the interface's "new" can return some reasonable default implementation, or decide among implementations at runtime.
You'd still need to recompile everything, so that all callers would use the invokeinterface bytecode instead of invokevirtual. Alternatively, invokeinterface/invokevirtual could be collapsed into one opcode, and rely on polymorphic inline caches/hot spot inlining to remove most of the method dispatch overhead. A third option would be to have the bytecode verifier perform invokevirtual/invokeinterface substitution at class load time in cases where a concrete class has been changed to an interface or vice-versa.
Making "new" static factory methods instead of an operator would also make it possible for a class to transparently swap in specialized subclass implementations (for instance, specialization for BigInteregrs that fit in 64 bits, or Strings where all codepoints are less than 256). PyPy does a small amount of this specialization internally for Python, but it would be nice to have language-level support.