Skip to content
Chapter 02 · JUnit 5 & Mockito

What Unit Testing Is: Unit, Integration and Functional Tests ​

The roadmap described what unit testing is for. This chapter makes it precise, with examples: what a unit is, how to test it in isolation, and where unit tests stop and integration and functional tests begin.

What is unit testing? ​

A unit test tests an individual unit, a method or function, in isolation.

java
public class Calculator {
    public int calculateMultiply(int a, int b) {
        return a * b;
    }
}

A unit test for calculateMultiply calls it with known input and checks the output: for 4 and 2 it must return 8. (How to write the test itself comes in the next chapters.)

In isolation ​

Now the method gets its result from another class:

java
public class Calculator {

    private NumberUtils numUtil;

    public Calculator(NumberUtils numUtil) {
        this.numUtil = numUtil;
    }

    public int calculateMultiply(int a, int b) {
        return numUtil.multiply(a, b);   // calls another class
    }
}

public class NumberUtils {
    public int multiply(int a, int b) {
        return a * b;
    }
}

When testing Calculator, the goal is to test calculateMultiply only, not NumberUtils.multiply. That method could itself be large, with dependencies of its own that call yet other classes; a failing test would then not tell you whose code is wrong.

So the call to the other class is mocked: instead of running the real method, the test tells the mocking framework "when numUtil.multiply(4, 2) is called, just return 8, without running the real logic." With Mockito (covered later in this series), that reads:

java
when(numUtil.multiply(4, 2)).thenReturn(8);

What about a method that calls a private method of its own class? Do not mock that. Splitting one method's logic into smaller private methods keeps it readable, but it is still the same unit's logic; mocking it would mean not testing your own code. Mock only calls that leave the class.

Why unit tests matter ​

Unit tests are the first line of defense against bugs:

  • Early bug detection. If a method returns a wrong value (say 7 instead of 8, because someone changed a * b to a * b - 1), the test catches it while you are coding, not after release.
  • Refactor confidently. Rewrite a loop as a stream, and the input and output should stay the same. If the new code behaves differently, the existing tests fail and tell you, so nothing breaks silently.
  • Documentation. A test shows how a method behaves ("this input gives this output") without reading all its code.
  • Lower cost. Fixing a bug during development is far cheaper than fixing it after the application goes live.

Unit vs integration vs functional testing ​

The three differ in scope:

Unit testing ​

One method of one class; everything outside it is mocked.

Integration testing ​

Tests multiple modules or components together, to check that the system behaves properly when wired together. The goal is not the end-to-end user behavior; it is whether the wired parts achieve the intended behavior.

Use case 1: modules of one component. Service → Repository → DB:

  • call a method in the service layer
  • check that Spring's dependency injection wires things correctly (nothing mocked between these modules)
  • the flow should reach the database layer
  • assert that the data was really stored in the database

Calls to external services are stubbed or mocked: the goal is to test the modules of this one component, not other microservices.

Use case 2: a few components (not the whole system):

  • Service A → Service B: check that A calls B over REST (RestTemplate, Feign, ...), including response mapping and HTTP error handling
  • Service A → Kafka: check that a message is published to the topic
  • Service A → mail: check that the email payload is built and sent correctly

Functional testing ​

End-to-end testing of a business feature, checking that it works as expected. The internals are a black box; you send a request and check the result.

Use case 1: one component, from its clients' view. Call its API, for example /payment:

text
API → Controller → Service → (real external service calls) → DB

You do not check whether the database insert or the mail happened directly; you check the response: are the expected fields there, is there a transaction id, is mailSent true?

Use case 2: the whole system, from the end user's view. All components involved in the feature are deployed to a stage or QA environment, and the feature is tested the way a user would use it, from the UI or by calling the entry API: frontend, backend, databases, Kafka and so on.

Frameworks ​

  • Testing frameworks: JUnit, TestNG
  • Mocking frameworks: Mockito, EasyMock

JUnit with Mockito is the most popular combination, and the one this series uses.

Next: Before writing tests with JUnit 5, it helps to know what runs them. Chapter 3 shows how JUnit 5 is built: the Platform, the Jupiter and Vintage engines, and what happens when you click Run or call mvn test.

Check yourself ​

  1. What is a "unit" in unit testing?
  2. Calculator.calculateMultiply calls NumberUtils.multiply. What do you do with that call when unit testing Calculator, and why?
  3. Your method calls a private helper in the same class. Should the test mock it?
  4. Give the four advantages of unit testing.
  5. What is the goal of an integration test, and what is it not?
  6. In an integration test of Service → Repository → DB, what is mocked and what is not?
  7. How does a functional test check that a payment API works?
  8. Order unit, integration and functional testing by scope.
Answers
  1. An individual method or function, tested in isolation.
  2. Mock it ("when called with 4 and 2, return 8"), so only calculateMultiply's own logic is tested.
  3. No: it is part of the same unit's logic; mock only calls to other classes.
  4. Early bug detection, confident refactoring, documentation, lower cost.
  5. To check that modules or components wired together achieve the intended behavior; not to test end-to-end user behavior.
  6. Nothing between service, repository and database is mocked (real wiring and a real DB); external services are stubbed or mocked.
  7. It calls the API and validates the response (fields, transaction id, flags), treating the internals as a black box.
  8. Unit (smallest), integration, functional (widest).