Icon for gfsd IntelliJ IDEA

Software testing basics: unit testing vs integration testing

The difference and relationshop between unit testing and integration testing

Unit testing and integration testing are both indispensable in software quality assurance. This post covers the basics of both types of testing and introduces a useful tool to automate your testing activities.

For any team developing applications, thoroughly testing software is a challenge. Developers and testers apply various types of testing on multiple levels to catch all potential issues, all of which are necessary to ensure high software quality.

While there are various types of testing, we identify three key layers of testing software: unit, integration, and system testing (with acceptance testing sometimes added on top). The testing pyramid depicts how these layers of testing build on top of each other. This article focuses on unit testing and integration testing, their differences, and the practical aspects of applying them in your projects. We’ll also be introducing a useful tool to cut the time and effort costs of testing so that you can focus on business logic and the fun of building software!

Testing pyramid

What is unit testing?

As an important element of a sound testing strategy, unit testing focuses on checking the smallest building blocks of your application. Unit tests are used to test individual functions, lines of code, methods, or classes in isolation from the rest of your code. They are at the bottom of the pyramid since they target the smallest components of your application – meaning that if you do unit testing, you’ll be using lots of them to check if each small code block works as expected.

An illustration about unit testing

The main benefit of unit tests is that they barely require any context: unit tests are run on the smallest components, regardless of the rest of your application, and without relying on any external systems. With unit tests, you won’t be worrying about accessing external databases, configuring systems, or exchanging data across networks. Unit tests are simple to write, fast to run, and easy to debug. Despite being so simple, unit testing can help pinpoint issues during programming to catch issues in real time as code is being written.

That said, they obviously can’t catch any issues that are related to the interactions between system components or different systems. That’s what integration testing is for.

What is integration testing?

Integration testing is one step up the testing pyramid. It is used to test the interaction between two or more components (each of which has ideally been tested separately with unit tests). Any of your application’s interfaces to databases, instances of accessing other software systems or external hardware should undergo integration testing to check the flow of data between and the interactions of these different components.

In broad terms, integration testing can be conducted in two key ways, namely: big bang testing (when all the components are integrated and then tested together as a unit), and incremental integration testing. The latter can happen in 3 primary ways:

  • The bottom-up approach means that you’ll be testing lower-level modules first, which then helps to test higher-level components.
  • As you may have guessed, the top-down approach is the exact opposite: testing follows the control flow of the system, with the topmost module tested first, and then integrating lower-level elements one by one.
  • The mixed (aka sandwich) approach is a hybrid one, combining the previous two: higher-level components are integrated with lower-level modules, then tested together as a system.

Should you use unit testing or integration testing?

A key thing to understand is that this is not really a “vs” situation: the question is not which one to use, but when to apply which one during development. Both unit testing and integration testing are crucial to ensure that your software application works as expected.

Without unit testing, even trivial bugs have a chance to slip through the cracks and cause trouble downstream. Without integration testing, each individual unit may work just fine – but when combined, the system may still fail. Skip any of these types of testing, and you may end up with an application that doesn’t work as expected.

Symflower for unit and integration testing

As these are the bottom and middle tiers of the testing pyramid, you’ll inevitably be using a good number of unit and integration tests. Writing all these tests manually can be a huge productivity drainer. But there’s no shortcut around thorough testing: either you make time for it now, or you deal with the consequences later. So how do you optimize the effort that goes into testing? With smart tooling, of course.

Symflower is a smart Java unit test generator that generates unit tests and testing code for integration tests so that you can focus on more challenging development activities. It can generate Java test templates to be used as boilerplate code for your unit or integration tests; generate complete (JUnit) unit test suites with meaningful values; and even provide test-backed diagnostics right in your IDE as you code.

Let’s dive into how you can use Symflower to accelerate your unit and integration testing.

Types of tests in Symflower

The primary goal of Symflower is to generate ready-to-run JUnit test suites with meaningful values. The tool is able to generate three main types of tests. Below, we’ll describe each one, and showcase a quick code example based on the following simple function:

class Copy {
	static String[] copy(String[] from, String[] to) {
		for (int i = 0; i < from.length; i++) {
			to[i] = from[i];
		}
		return to;
	}
}

Generating standard unit tests with Symflower

Symflower will analyze your code with mathematical models (based on symbolic execution). It will then generate meaningful test values that execute all possible paths in your code with unit tests. The tool generates a slim JUnit test suite with only essential test cases that use targeted values to cover all relevant paths.

For the above copy function, Symflower will automatically generate the following Java unit test suite:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class temp class CopySymflowerTest {
    @Test
    public void copy1() {
        String[] from = null;
        String[] to = null;
        // assertThrows(java.lang.NullPointerException.class, () -> {
        Copy.copy(from, to);
        // });
    }
    @Test
    public void copy2() {
        String[] from = {};
        String[] to = null;
        String[] actual = Copy.copy(from, to);
        assertNull(actual);
    }
    @Test
    public void copy3() {
        String[] from = { null };
        String[] to = null;
        // assertThrows(java.lang.NullPointerException.class, () -> {
        Copy.copy(from, to);
        // });
    }
    @Test
    public void copy4() {
        String[] from = { null };
        String[] to = { null };
        String[] expected = { null };
        String[] actual = Copy.copy(from, to);
        assertArrayEquals(expected, actual);
    }
    @Test
    public void copy5() {
        String[] from = { "" };
        String[] to = {};
        // assertThrows(ArrayIndexOutOfBoundsException.class, () -> {
        Copy.copy(from, to);
        // });
    }
}

Generating mocked unit tests with Symflower

Symflower is also able to analyze concepts such as complex data types, interfaces, and object orientation. It can automatically generate mocks for the interfaces that your code uses, which means you won’t have to spend time specifying the behavior of these elements.

Here’s an example of an application whose generated tests will include mocked elements:

public class Mocking {
    public static boolean matchingLegs(int i, Animal a) {
        return i == a.getNumberOfLegs();
    }

    public static String compare(Animal a, Animal b) {
        if (a.compare(b) == 0) {
            return "equal";
        } else if (a.compare(b) < 0) {
            return "weaker";
        } else {
            return "stronger";
        }
    }

    public static int legsBySound(Animal a) {
        String s = a.getSound();
        switch (s) {
        case "meou":
            return 1;
        case "kikariki":
            return 2;
        default:
            a.stroke();
        }

        return a.getNumberOfLegs();
    }

    public static boolean feed(int i, Animal a) {
        boolean hungry = a.feed(i);

        while (hungry) {
            a.stroke();
            hungry = a.feed(i);
        }

        return hungry;
    }
}

interface Animal {
    int getNumberOfLegs();

    String getSound();

    int compare(Animal a);

    Animal reproduce();

    void stroke();

    boolean feed(int i);
}

Here’s an excerpt of the test suite generated for the above code:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class MockingSymflowerTest {
    @Test
    public void legsBySound1() {
        Animal a = mock(Animal.class);
        when(a.getSound()).thenReturn(null);
        // assertThrows(java.lang.NullPointerException.class, () -> {
        Mocking.legsBySound(a);
        // });
    }

    @Test
    public void legsBySound2() {
        Animal a = mock(Animal.class);
        String parameter = "kikariki";
        when(a.getSound()).thenReturn(parameter);
        int expected = 2;
        int actual = Mocking.legsBySound(a);

        assertEquals(expected, actual);

        verify(a, times(1)).getSound();
    }

After generating tests, you’ll be able to review and select the test cases you want to add to your test suite.

Generating Spring Boot tests with Symflower

For Spring Boot users, Symflower offers unit test templates that come with auto-wiring, with all other used components mocked by default. These test templates include all the necessary imports, annotations, object initializations, function calls, asserts, and more. All you’ll need to do is fill in the right values for your test scenario.

If you’re looking to perform integration tests instead of unit tests, you can just replace the mocked parts in the auto-wired templates and define a more sophisticated test configuration. Thus, Symflower’s test templates are a good starting point for both unit and integration tests.

Let’s see an example! Here’s the code we’ll be working with:

package org.springframework.samples.petclinic.vet;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author Juergen Hoeller
 * @author Mark Fisher
 * @author Ken Krebs
 * @author Arjen Poutsma
 */

@Controller
class VetController {

	private final VetRepository vetRepository;

	public VetController(VetRepository clinicService) {
		this.vetRepository = clinicService;
	}

	@GetMapping("/vets.html")
	public String showVetList(@RequestParam(defaultValue = "1") int page, Model model) {
		// Here we are returning an object of type 'Vets' rather than a collection of Vet
		// objects so it is simpler for Object-Xml mapping
		Vets vets = new Vets();
		Page<Vet> paginated = findPaginated(page);
		vets.getVetList().addAll(paginated.toList());
		return addPaginationModel(page, paginated, model);

	}
}

Source

For the above code, Symflower would generate a test template that looks as follows:

package org.springframework.samples.petclinic.vet;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(VetController.class)
public class VetControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private VetRepository vetRepository;

    @Test
    public void showVetList() throws Exception {
        this.mockMvc.perform(get("/vets.html"))
            .andExpect(status().isOk())
            .andExpect(content().string(""));
    }
}

Whether you’re looking to deliver unit or integration tests, you can now adapt this template to suit your needs and use it as boilerplate code for further tests.

Looks interesting? Try Symflower in your IDE for free.

Don’t miss any of our upcoming pieces of content: sign up for our newsletter and follow us on Twitter, LinkedIn or Facebook!

| 2023-08-01