Best Practices for Unit Testing
Shreya Bose, Technical Content Writer at BrowserStack - May 18, 2021
What is a Unit Test?
A unit test verifies the functionality of the smallest possible module or “unit” of an application, independently from other modules. In this case, testers and/or developers isolate the smallest application components, check their behavior and identify defects early on in the development pipeline.
For example, in C#, consider a method a unit (smallest component to be tested). In this case, the unit test would verify some feature of the method in isolation from other methods i.e. the software at large.
Generally, unit tests comprise three phases:
- Initialize the application module to be tested AKA the system under test
- Apply stimulus to the system under tests
- Observe resultant behavior
If the module behaves as expected, the test passes. If not, the test fails, signaling that an error or anomaly exists somewhere in the system. In development nomenclature, these phases are sometimes referred to as Arrange, Act, and Assert.
Unit Tests are usually categorized as state-based and interaction-based testing. The former check to see if the software is producing expected results under specific conditions. The latter verifies if the software properly calls particular methods to accomplish its purpose.
Best Practices for Unit Testing
These unit testing best practices help streamline and structure tests for maximum efficiency and accuracy:
- One Assertion in One Test Method
Fundamentally, every test asserts a hypothesis – assuming that a certain action can be undertaken by the software. To keep unit tests simple, it is best to include a single assertion in one test method. That means, one unit test should test one use-case and no more. Now, QAs may try to test all aspects of a module with multiple assertions in one method so as to cover more features in one test. However, if a test with 10 assertions returns a single failure, testers will have to go through each assertion to figure out what exactly went wrong. Which of the 10 assertions failed? It is far more convenient to keep one assertion in one method. It may take a little longer to write the test script, but the results will be more definitive and save effort in the long run.
- Minimize Test Interdependence
Ensure that every test has its own setup and teardown mechanism. Test runners don’t follow a single particular order in which they run tests. That means test suites or classes cannot be trusted to maintain the ideal state between tests. Tests should be ready to run in any order on any machine without affecting any other test in the suite. Try to avoid dependencies on environmental factors (language settings of the machine, current time, etc.) or external conditions (network, file system, APIs) as well. Tests with dependencies are unstable, harder to execute, diagnose and debug.
For example, a test runner may run two tests in a particular order for a while. If a tester thinks that this order will be maintained, they might add a third test without adding setup. If the test runner changes order or runs the tests in parallel, it will disorient the entire test suite and cause failures due to sequencing issues. In this regard, Martin Fowler’s distinction between solitary and sociable code is useful. Solitary code is self-contained and doesn’t depend on other units to run. Sociable code interacts with other components to validate behavior and thus, requires dependencies.
- Automate Unit Tests
Testing every component of a complex, layered modern-day website or app cannot be a manual process. Tests need to be automated to run daily or even multiple times a day as part of a CI/CD pipeline. Human testers simply cannot run the required number of tests fast enough or accurately enough to roll out software with tight deadlines. They must resort to automation testing for this purpose. Learn why unit tests form the base of the testing pyramid.
- Use a Consistent Naming Convention
Unit tests need to be appropriately named to maintain code readability. Tests must be reasonable so that, in case of a test failure, QAs can quickly understand which particular module is malfunctioning. Test names should also be able to clearly convey their purpose to new QAs who might join the project. Certain programming languages like Kotlin allow testers to name methods in plain English. Other test automation frameworks like JUnit offer tags for naming purposes.
Every project or testing team has naming conventions of its own. They have to decide on naming standards that all members can quickly understand, even if they are joining in the middle of a project. Learn about the top unit testing frameworks in Selenium.
- Ensure tests are deterministic
Deterministic tests are ones that exhibit the same behaviour as long as their code is unchanged. For example, let’s say a unit test is built to verify function x(). The test passes and should continue to pass as long as x() remains unchanged. The same applies to any changes in x(). With changes, the test should always fail unless the changes are reversed. A deterministic test case should only change its output (from passing to failing or the reverse) because of changes in the production code it targets or changes in the test itself. In the latter situation, the test has bugs in itself which need to be ironed out before proceeding.
Sounds obvious, right? But in the world of software testing, false positives and negatives are frustratingly frequent. Any test that sometimes passes and sometimes fails cannot be trusted, and therefore does not verify system behavior. For unit tests to be deterministic, they must be completely isolated. This is why testers must be wary of including any test interdependencies.
To keep tests deterministic, QAs must execute them on real browsers and devices, not emulators and simulators. The latter comes with serious limitations in terms of replicating real-world conditions because they cannot mimic aspects like battery life, incoming calls, interactive features like pinch-and-zoom, etc. Without being exposed to actual production environments (real, functional real devices), test results will be nowhere close to deterministic or accurate.
Try Testing on Real Devices for Free
- Avoid Logic in Tests
Unit tests should be written with minimal to zero manual strings concatenation and logical conditions like while, if, switch, for, etc. Doing so reduces the chance of introducing bugs into the test. The focus must remain on the end result, not on circumventions of implementation details. Without too many conditions, tests are also likely to be deterministic.
Also Read: Learn how to write an effective bug report
- Write tests during development, not after it
Set up tests are early as possible in the sprint development cycle. This helps save time in the later stages by identifying bugs early on. In successful DevOps and Agile environments, QA comes much earlier into the picture. Development sprints do not begin without the specifications being reviewed by the QA team and accounting for their recommendations on questions of security, performance, and stability.
Do you know what is the role of QA in DevOps?
Incorporate the unit testing best practices discussed above will help make the tests cleaner, faster, and easier to execute. However, as far as possible, tests must be run in real user conditions to ensure complete accuracy of results.
Use real browsers and devices. It doesn’t matter what emulator or simulator is being used; they will not offer 100% accurate results. Additionally, given the extent of browser and device fragmentation in the digital world, every website will be accessed via multiple browser-device combinations.
Instead of setting up an expensive and effort-intensive on-premise device lab. leverage a cloud-based testing platform like BrowserStack that offers access to the latest and legacy devices, browsers, and operating systems. Users can access 2000+ real devices and browsers for manual testing, automated Selenium testing, and Cypress testing. For example, tests can be run on Chrome operating on a Samsung Galaxy S20, Firefox operating on Galaxy S20+, and the like. There are thousands of combinations to choose from and test on.