Unit testing - why, how and when
This article explains benefits from unit testing, what components we should test and give some directions to write better unit tests.
The article assumes that the reader knows how to create and run simple unit tests but doesn't know everything about unit testing and can find useful tips and points there.
Unit test is code for testing other code. It calls the code under testing and compares received results with expected:
public void testTotal() {
Bill bill = new Bill();
bill.addItem(new Item("Thinking in Java", "book", 29.50));
bill.setShippingPrice(15.50);
assertEquals(45.00, bill.getTotal());
}
You can execute unit test at any time to check if the functionaly works as expected.
Benefits from Unit testing
Perform testing frequently - automatic testing doesn't take a lot of time.
After the code changes we perform testing to guarantee that the program works as expected. If the product lifecycle isn't short then automatic testing is preferrable: we capture requirements in the code (unit tests) and can perform these tests after each code change.
So we can find out what's broken as early as possible and fix the problem immediately. We write tests once and run many times, and testing doesn't take a lot of time that almost impossible with manual testing.Also sometimes changes in one module require changes in other module or affect it but we don't know about it until we test other module. If we have unit tests, we can run them to check other modules. It's good to discover this in the middle of development from failed unit tests rather during testing.
Keep your code cleaner. Can perform refactoring without breaking the program.
We don't refactor our code because we might break some functionality with our code changes. With unit tests you do refactoring and run tests to see that the program works after changes correctly.Find exact place of error.
Testing from user interface is coarse grained: you check a case that consists from other small subcases. So you need additional efforts to find what doesn't work exactly if an error occurs.
For example, client bills generation in billing system is one call from the web page, but it includes several steps:
- get clients to which send bills
- compose bill for each client
- save bill into the database
- prepare PDF file for each bill by database data
If you receive an incorrect bill, you don't know where the error is exactly. It will be in bill creation, placing bill into the database or bill PDF generation by database.
In unit tests we can test these parts independently and on different conditions.
We can test PDF generation without bill composing module: we create a plain bill object, set it fields to proper values and pass it to the PDF generation function:
If the output is unexpected then there is an error in the PDF generation.Customer customer = new Customer(); customer.setAddress(new Address("UK", "London")); Bill bill = new Bill(); bill.setCustomer(customer); bill.addItem(new Item("Thinking in Java", "book", 29.50)); bill.setShippingPrice(15.50); bill.setCurrency(Currency.USD); Document doc = billPdfService.generate(bill); List<String> sections = doc.getSections(); assertEquals(4, sections.size()); assertEquals("London, UK", section.get(0)); assertEquals("Thinking in Java $29.50", section.get(1)); assertEquals("Shipping $15.50", section.get(2)); assertEquals("Total $45.00", section.get(3));
Reuse your test data.
When we test functionality through user interface, such test needs some preparations: we should create objects needed for test (input data). For example, search engine testing requires objects with different properties for searching and search queries. Also it requires manual checking of output data: what objects are included in search results.Unit test also requires test preparations but only once during test creation. UI testing requires test preparation before each run and manual results checking after it.
Requirements for test data: test data should be the same on each test run.
So this and another tests should perform cleanup: delete created objects that affect next test runs.
For example, delete created bill from the database because it can be treated as input bill for another test. Or delete file created by the test because if we don't delete the file then the test fails on next run with file creation error since there is a file with same name.Just do it, or unit test is good start point to encourage working on the feature.
Sometimes you write a lot of code and can't run it immediately since it requires integration with other modules, creation of web pages or database objects. You can run the code in the unit test immediately after its creation and test it under different conditions. Seeing that the code works, really encourages!Also if we write the unit test before the code, we understand more clearly how to call this code from other parts of applications: input parameters contraints and what methods to call. We create more usable and predictable code.
That's why we write unit tests first.
What code we should test in unit tests.
- complicated logic like billing, search engines, text parsing, system states and transitions between them
- code that can't be tested from UI directly: requires additional UI pages, special input data (like steps in multi-step process), or called by timer
- a lot of test cases that require a lot of time if perform manually (a lot of variants for input data, for example text parsing). Such testing from the code is much easier: unit test can use loops and method calls with parameters for sets of similar test cases.
How to write better unit tests.
Keep your user interface code and controllers simple, move all complicated logic into separate model or business logic objects which it's easier to test. Controller for the client bill generation from example above can look like:
void generateBill() {
List clients = getClients();
List bills = createBills(clients);
saveBills(bills);
generatePdf(); // reads bills from the database
}
and test each of these functions.
Unit testing is most useful if you test business and model objects with minimal dependencies or algorithms without dependencies.
Prefer pure objects that don't inherit framework classes to minimize dependencies if you need a lot of code to setup and work with the framework.
Avoid database access or other external services usage. It's easier to create tests
because we have less additional objects to create or setup. And these objects introduce own errors too.
It can be incorrect database setup or errors in the database access code (other module of the system).
It's better if your domain objects are plain objects that can be created without database.
Or use same separate test database for all tests to reduce number of errors due with dependency.
Another problem is dependencies from other modules. You can use stubs with same interfaces instead real objects. These stubs can do nothing if it doesn't affect your test (example: send email) or return predefined same results without complicated calculations or database access. You don't have to create these stubs manually: use mock objects for automatic creation.
Conclusion:
Unit tests help to perform testing frequently, find the problems early and their location with minimal efforts, refactor the code, understand what it does, increase the code testing coverage and it's a good start point to obtain a clean working code as earlier as possible.Unit testing is most useful if you test business and model objects with minimal dependencies or algorithms without dependencies.