How to get started writing unit tests on a large complex codebase without existing tests

A little bit at a time.

Developing unit tests for a large scale application can be challenging. Especially if it was written without any tests, or with testing in mind.

Writing unit tests requires code to be modular — so you can test a unit in isolation. One of the biggest benefits of writing unit tests during development is not that your code will be well tested, but that it helps you think about writing it in a way that different parts of the application (units) can be tested without interacting with the whole application.

But if it hasn’t been written that way, it may require significant refactoring to be testable. Or it may require mocking, or creating fake services, or injected databases with dummy data. And this can make tests complex, brittle, and unreliable.

So a good way to start unit testing on a code base that is resistant to testing in isolation because it is complex & tightly coupled, is to add unit tests slowly, as you modify the codebase.

Find an area that is complex (and ideally has a bug that needs to be fixed). Once you identify the piece of code that you need to test (by looking at log files, stepping through it with a debugger, or adding “print” statements), work on recreating that manual verification process in a unit test.

Write a test that describes behavior you want to test (or defect you want to expose.)

Unit tests should not depend on a deployed application, external services, or need you to create many mocks or stubs. You do not want to do this, for the reasons mentioned above.

So if you can find a piece of logic, isolate it, and refactor the code so that you can test this in isolation.

You may need to mock 1 or 2 external interfaces, but don’t go overboard. Too many dependencies is a sign that your code is too tightly couples. If you find this is the case, you may want to start with unit tests in an area that is simpler to isolate. You can tackle the more complex areas of your codebase later — when you have more tests written, and refactoring it becomes less risky.

Another thing you do not want to do is create a bunch of simple unit tests for easy methods that have little or no logic — getters & setters, constructors, helper methods that just hide ugly APIs with verbose setup steps, etc. If you have code that is very unlikely to ever change or break, don’t write unit tests for it. Every test you write adds to your codebase, which means adding to maintenance cost. Don’t write unit tests just to increase numbers or code coverage. Make every test count.

One last thing to avoid is writing unit test that describe functionality or depend on the implementation. Tests with a lot of mocks or data setup or several assertions are probably because of this. By writing tests to the implementation, you may be verifying that it works as intended, but it will make it hard to refactor the codebase in the future. A lot of projects abandon unit testing when they find that the existing tests cause friction in refactoring, or worse, tests that end up getting ignored because they are not keeping up to date.

So to reiterate, the answer to getting started writing unit tests on a large existing codebase is to start slowly, adding tests as you work on specific areas of the codebase, refactoring the code as you work to make testing in isolation easier, and avoiding (at first) areas that are too difficult to break down into unit tests, and areas that are too simple to ever fail. Make ever test count, and make it tell you something useful, and don’t set up code coverage goals or write more tests just to increase numbers.

Here is the question on Quora that this answer addresses: