Unit testing and the most common mistakes

Unit testing definition (unit test definition)

Unit testing is a type of software testing that focuses on individual small pieces of code or components of a software system. The goal of unit testing is to verify that each small part – unit of the software works as it should and meets the requirements. Unit testing is usually performed by developers and is done at the beginning of the development process before the code is integrated and tested as a whole.

Unit tests are automated and are performed whenever code changes to ensure that the new code does not break existing functionality. Unit tests are designed to verify the smallest possible unit of code, such as a function or method, and test it in isolation from the rest of the system. This allows developers to quickly identify and fix potential problems early in the development process, improving the overall quality of the software and reducing the time required for later testing.

Unit test vs integration test

When discussing unit testing, it is essential to distinguish differences between unit tests and integration tests. While unit tests focus on testing individual components or units of code in isolation, integration tests verify the interactions between these units and ensure that they work together as expected.

In the STLC(SDLC) or V model, unit testing is the first level of testing performed before integration testing.

Unit test benefits

  • Unit testing allows the programmer to refine the code and make sure the component is working properly.
  • It allows you to test parts of the project without need of waiting for the other parts to be finished.
  • It allows developers to detect and fix problems early in development before they become bigger and harder to fix.
  • Unit testing helps ensure that each unit of code works as it should and meets the requirements, improving the overall quality of the software.
  • It allows developers to work faster and more efficiently because they can verify code changes without having to wait for the entire system to be tested.
  • It provides clear and concise documentation of the code and its behavior, making it easier for other developers to understand and maintain the software.
  • Unit testing allows developers to safely make changes to code because they can verify that their changes will not break existing functionality.
  • It can reduce the time and cost of later testing by helping to identify and fix problems early in development.

Disadvantages of unit testing

  • The process of writing unit test cases is time consuming.
  • Unit testing will not cover all the bugs in a module because there is a possibility of bugs in modules during integration testing.
  • It is not effective for error checking in the user interface (UI) part of the module.
  • It requires more time for maintenance when the source code changes frequently.
  • The success of unit testing depends on developers, who need to write clear, concise and comprehensive test cases to validate the code.
  • Unit testing can be challenging when dealing with complex units, as it can be difficult to isolate and test individual units separately from the rest of the system.

Unit test types

There are 2 types of unit testing: manual and automated.

Manual unit testing is a practical approach where testers write and execute test cases without the help of automation or unit testing tools. This type of unit testing is often more flexible and can be more concise in certain contexts. However, it is generally more time-consuming and prone to human error.

Automated unit testing:

The developer writes a piece of code in the application just to test the feature. Later, he will comment out the test code and finally remove it when the application is deployed. Using an automation framework, the developer programs criteria into the test to verify the correctness of the code. During test case execution, the framework logs failed test cases. Many frameworks also automatically flag and report these failed test cases in summary. Depending on the severity of the failure, the framework may stop further testing.

The unit testing workflow: 1) Creating test cases 2) Review 3) Basic evaluation 4) Execution of test cases

Unit test methods

There are 3 types of unit testing methods. They are:

Black Box Testing: this testing technique is used when testing components for input, user and output parts.

White Box Testing: this technique is used to test the functional behavior of a system by inputting the input and checking the output of the functionality, including the internal structure of the design and the code of the modules.

Gray Box Testing: this technique is used in the execution of relevant test cases, test methods and test functions, and in the analysis of the code performance of modules.

Anatomy of a unit test

1. Test fixtures (test assumptions)

Test fixtures are the components of a unit test responsible for preparing the environment needed to execute the test case. These are the prerequisites and settings you need to run the test cases. They are also called “test context” and create initial states for the unit under test to ensure more controlled execution. They are very important because they provide a consistent environment to repeat the testing process.

For example, let’s say we have a blogging application and we want to test the post creation module. Test fixtures should include:

  • Connecting to the posts database
  • Sample blog post with headings, table of contents, author information, etc.
  • Temporary storage for processing post attachments
  • Test configuration settings (default post visibility, formatting options, etc.)
  • Test user data

2. Test case

A unit test case is simply a piece of code designed to verify the behavior of another unit of code, ensuring that the unit under test performs as expected and produces the desired results. Here is a unit test example of a function that calculates the sum of two numbers a and b:

use PHPUnit\Framework\TestCase;
class MathTest extends TestCase {
public function testSum() {
// Premenné
$a = 5;
$b = 7;
$expectedResult = 12;
// Funkcia
$result = Math::sum($a, $b);
// Assertion - tvrdenie
$this->assertEquals($expectedResult, $result);
}
}

The assertion used in this code is $this->assertEquals($expectedResult, $result); it verifies that a + b actually equals the expected result 12.

3. Test runner

Test runner is a framework for organizing the execution of multiple unit tests and also for reporting and analyzing test results. It can search the codebase or directories to find test cases and then execute them. The great thing is that the test runner can run tests according to priority while managing the test environment and handling setup/teardown operations. Thanks to the test runner, the unit under test can be isolated from external dependencies.

4. Test data

Test data should be carefully selected to cover as many scenarios as possible for a given unit, thus ensuring high test coverage. In general, we should prepare data for:

  • normal cases: typical and expected input values for a given unit
  • borderline cases: input values at the limit of the acceptable range
  • invalid/error cases: invalid input values to see how the unit responds to errors (with error messages or certain behaviours)
  • edge cases: input values representing extreme scenarios that have a significant impact on the unit or system

5. Mocking and Stubbing

Mocking and stubbing are essentially surrogates for the actual dependencies of the unit under test. When unit testing, developers need to focus on testing a specific unit in isolation, but in certain scenarios they will need two units to perform a test.

For example, we can have a User class that depends on an external EmailSender class to send email notifications. The User class has a sendWelcomeEmail() method that calls the EmailSender to send a welcome email to a newly registered user. If we want to test the sendWelcomeEmail() method in isolation without actually sending the email, we can create a dummy object of the EmailSender class. The developer will then not have to worry about whether the external unit (EmailSender) works well or not. The unit under test is indeed tested in isolation.

Unit test software tools

Here are some commonly used tools for unit testing:

  • Junit (for a unit test in java): Junit is a free testing tool used for the Java programming language. Provides assertions to identify the test method. This tool first tests the data and then inserts it into a section of code.
  • NUnit: NUnit is a widely used unit testing framework that is used for all .net languages. It is an open source tool that allows manual scripting. It supports data-driven tests that can run in parallel.
  • JMockit: JMockit is an open source unit testing tool. It’s a code coverage tool with line and path metrics. Enables API mocking with logging and authentication syntax. This tool offers row coverage, path coverage and data coverage.
  • EMMA: EMMA is an open-source toolkit for analyzing and reporting code written in Java. Emma supports coverage types like method, line, basic block. It is Java-based, so it is free of dependencies on external libraries and can access the source code.
  • PHPUnit: PHPUnit is a unit testing tool for PHP programmers. The tool allows developers to use predefined assertion methods to confirm that a system behaves in a certain way.
  • PyUnit (for a unit test in python), also known as Unittest, is a JUnit-inspired unit testing framework included with Python installations. It provides an easy and fast way to run test cases without the need for additional installations.
  • MSTest / Visual Studio (for a unit test in c#):MSTest is a unit testing framework that is part of Visual Studio, a popular integrated development environment (IDE) for .NET. MSTest simplifies the unit testing process by integrating directly into the Visual Studio IDE, which is convenient for developers using Microsoft.

What do unit tests look like?

A unit can be almost anything you want – a line of code, a method, or a class. In general, however, smaller is better. Smaller tests give you a much more detailed view of how your code works. There’s also the practical aspect that when you’re testing very small units, tests can be run quickly, like a thousand tests per second.

Consider this code sample:

def divider (a, b)

  return a/b

end

 

Using Ruby, these small tests might look like this:

class smallTest < MiniTest::Unit::testCase

  def tiny_test

    @a=9

    @b=3

    assert_equal(3, divider(a, b))

  end

end

This example is too simple, but it gives an idea of what I mean by small. If you want to see the direct procedure of creating a unit test, click here unit test tutorial.

Unit test life cycle

Unit tests usually consist of three phases:

  • Planning – developers consider which units in the code they need to test and how to execute all the relevant functions of each unit to test it effectively.
  • Test cases and scripts – developers write unit test code and prepare scripts to execute the code.
  • Unit testing and results – Finally, a unit test is run and developers can identify bugs or issues in the code and fix them.

Unit test best practices

1. Write appropriate test names

The basic thing to consider when writing a test is the choice of test title. Good test names improve the readability of the code both for the programmer and for others who may work on the code in the future. There are standard naming conventions (read more here – unit test naming convention) that can be used in unit testing.

2. Create simple tests

Keeping test codes as simple as possible is key to maintaining their correctness. Unit test codes can also have bugs, especially at high levels of complexity.

3. Create deterministic tests

The deterministic test always gives the same result regardless of the input, as long as the code does not change. This minimises the occurrence of false positives and false negatives. Tests must be deterministic because a test that presents variable results cannot be trusted.

4. Solving one use case

Each test should be used to test one use case. A particular test program should test a single block of code. This verifies the output and gives a better insight into the cause of the detected errors without any doubt to where they originated.

5. Strive for maximum test coverage

Developers should fully and thoroughly test the software application to the greatest extent possible. However, this is not always feasible due to time and financial requirements. Nevertheless, developers must try to perform unit tests of the program as much as possible.

6. Design unit tests to be as fast as possible

Slow tests are difficult for developers to perform. They slow down the process and cannot be used frequently. But it is true that test speed is subjective and depends on the subject being tested, but any test that takes more than an hour and 15 minutes can be classified as slow.

7. Accept test automation

Although unit tests can be performed manually, current procedures support an automated method of testing. It has proven to be not only more efficient and cheaper but also more time-saving.

The most common mistakes in unit testing and how to avoid them

Failure to keep tests up to date

Tests created months ago may no longer be valid. It is very important to perform tests regularly, as changes in requirements and code can cause problems that don’t match what they should test. Neglecting to update tests can lead to false or misleading results.

Solution: regular testing is essential. Preferably using automated unit testing. You should also make sure that tests are compliant with requirments.

Tests created months ago may no longer be valid.

Quantity of tests

One of the major mistakes in unit testing is not finding the right balance. Testing every line of code can be exhaustive and time-consuming, while testing too few parts can cause critical bugs to be overlooked. It is essential to focus on testing the most important features and edge cases.

Solution: use techniques such as code coverage analysis and risk assessment to identify the parts of the code that need the most testing. Prioritize testing complex features, error-prone areas and user-centric features.

Testing bad data

The use of incorrect or inappropriate data for testing can lead to misleading results. Such examples are false positives or negatives. For example, conducting tests using null values could mask real errors or different scenarios. Similarly, running tests with too similar and very random data could reduce the efficiency and transparency of your testing processes.

Solution: it is essential to use practical and inclusive data that includes a variety of situations and inputs. Use data generators and test data checking to prevent repetition.

Testing for incorrect data types

A significant pitfall in unit testing is checking incorrect aspects or evaluating something that is not a unit. Evaluating non-unit components such as database queries, web service calls, or user interfaces leads to unreliable and slow tests that rely on external elements.

Solution: use mocking and stubs for efficient unit testing. These techniques simulate unit dependencies, creating a controlled environment that isolates the unit from the larger system.

No peer reviews

Omitting test reviews means that it increases the likelihood of test errors. Code reviews increase the quality of unit tests in the team.

Solution: for optimal results, perform code reviews directly, either in person or via screen sharing.

Creating logic tests

Inserting logic into unit tests makes them harder to read and maintain and increases the risk of errors. If your unit test contains logic, it suggests that you may not be creating the right unit tests.

Solution: if there is a bug in the test units, it needs to be removed: Limit the number of assertions in each test. If you have too many assertions, maintenance becomes difficult.

Testing externalities

One reliable way to have unit tests that take forever to run is to write unit tests that do things like write files to disk or pull information from databases. So avoid using external elements because it will slow down the test and also because when you do that, you’re not actually writing unit tests. Unit tests are targeted checks that isolate code and assert how it should behave. Check things like “if I pass 2 and 2 to the add(int, int) method, does it return 4?”. This is the scope of the unit test.

Solution: Solution is easy. Don’t test databases, write to disk and so on, test only small parts of the code.

Conclusion

As you can see, unit testing can be very challenging. But component testing is always necessary at some level. That’s for sure.

If you want to test your knowledge of unit tests or are preparing for an interview, be sure to check out this list of frequently asked questions and answers – unit test questions for interview.

If you speak German and are looking for a job as an IT tester, take a look at our employee benefits and respond to the latest job offers.

About the author

Michaela Kojnoková

Agile Test Engineer

Po štúdiu informatiky na ŽU a TUKE som sa najviac ponorila do oblasti automatizácie testovania. Okrem toho sa venujem tvorbe webov, databázam, dátovej analytike, umelej inteligencii a strojovému učeniu. Mám rada cestovanie, šport a najviac si užívam čas strávený v prírode s mojimi blízkymi. LinkedIn

Let us know about you