DEV Community

Discussion on: What are your biggest problems with unit testing?

Collapse
alexbunardzic profile image
Alex Bunardzic • Edited on

"How does dependency injection works?"

You want to avoid tight coupling, so instead of touching the implemented class in your code, you only work with the interface. Interface is at a higher level of abstraction, therefore less tightly coupled with your code. In addition to that, you never want to instantiate the dependency in you code (that would result in tight coupling). Instead, you expect the instantiated dependency to be 'injected' into your code in the form of an interface. That way, you shield your code from dependencies.

"How can it be used for mocking, (example: mocking a file read/write or a network request to avoid using real I/O)"

If you use the class that is responsible for doing the I/O precessing, you have tightly coupled your logic with that class. Instead of manipulating the instantiated class in your code, send messages to the interface. For example, instead of directly invoking methods on your ORM class, use the IRepo interface. That way, you can in your unit tests replace the concrete class that is actually accessing the I/O with a fake class that is an impostor, or a stand-in for the real class.

"What to test? Only public methods, also private methods?"

Do not couple your tests with the code structure. Only couple your tests with the expected code behaviour. One of the biggest mistakes in software engineering is tightly coupling one's tests to the code structure. It is a very costly mistake that results in poor code quality and amasses a lot of technical debt.

"Should you use defensive programming? Make some function check for all possible wrong inputs, meaning you should also unit test all these code paths?"

No. Never do that. You only test the expected outputs. And those expectations must be documented in the acceptance criteria of the user story you're working on.

"What is code coverage? should you aim for some specific number of line or branches coverage?"

Code coverage conveys very little useful information. Instead of using the code coverage metric, switch to using the percentage of killed mutants in your code. Use mutation testing -- it is your best friend.

"Where is the frontier between unit test / integration test?"

The frontier between unit testing and integration testing is computer memory. Unit tests must only run in memory. When it comes to testing delivery channels or any other underlying computing infrastructure (disc I/O, databases, screens, browsers, or any other commodities), use integration testing.

"Should you use TDD? Always code the tests first?"

Yes. Do not test code, code to the test. That is the best way to go. Skipping on TDD is equivalent to the problem with doing open heart surgery and skipping on washing your hands because, hey, the patient needs urgent intervention. Skipping on writing unit tests first is a lame excuse.

"How to test mathematical functions where you can have a wide range of parameters and edge cases?"

Whenever you write any block of code, you are doing it because you have previously formulated some kind of an expectation. You expect that some event will be handled by your code and that event will include some values, and those values will get transformed somehow. You cannot start coding without having such expectations. The expectations may be only in your head, or maybe written down in some formal spec. Whatever it is, you must first write the unit test that describes the expectations. Once you do that, you write the code to satisfy those expectations. No need to do anything else (observe and respect the YAGNI principle).

"How to handle flaky tests (tests that don't always pass when you restart them, probably because of some environment/context dependency)?"

Flaky tests are an indication of tightly coupled code. You need to refactor relentlessly.

"How to properly manage environment setup/teardown to garantee test independence (tests should be run in any random order and still pass)"

Unit tests must never be dependent on the environment. They only run in memory, so they must always produce exact same results, regardless in which environment they run.