Kihagyás

Unit and Integration Testing

Unit testing is the most fundamental form of software testing, aiming to verify the smallest independently testable units (methods, classes, components) of a program in isolation.

Goal: to ensure that individual units work correctly independently of the rest of the system.

Characteristics:

  • Tests are created during the development phase.
  • They provide fast feedback on defects.
  • They can be automated (part of CI/CD pipelines).
  • Early detection of defects reduces development costs.

When running unit tests, the root cause of failures can easily be traced back to a single method or logical unit, which speeds up fixing.

Integration testing is the next step after unit tests.
Its purpose is to verify that modules communicating with each other (e.g., services, database layer, APIs) work correctly together.

Characteristics:

  • Examines interfaces and data flow between modules.
  • Typical sources of errors: data type mismatches, incorrect API calls, missing initialization.
  • Several integration strategies exist:
    • Big Bang Integration – integrate all components at once (rarely recommended).
    • Top-down / Bottom-up – gradual, hierarchical testing.
    • Continuous Integration Testing – a modern approach, part of the CI pipeline.

Defects often lie not in the individual components but in their interactions, which makes integration tests especially important in complex systems.

JUnit

JUnit 5 is a modern, Java-based unit testing framework that supports Java 8 and later. JUnit is part of the xUnit family of test frameworks and is widely used in both development and DevOps environments.

JUnit 5 architecture (three layers):

  • JUnit Platform – provides the runtime environment (IDE, Maven, Gradle integration).
  • JUnit Jupiter – the new programming and extension model (new annotations such as @Test, @DisplayName, etc.).
  • JUnit Vintage – provides compatibility with older JUnit 3–4 tests.

Features

  • Uses reflection to automatically detect test methods.
  • Easily extensible and integrable into Maven/Gradle build pipelines.
  • Supports parameterized and repeated tests.
  • External dependencies (database, API) can be simulated with mock objects.
  • Compatible with other frameworks (e.g., Mockito, Spring Test, AssertJ).

What is a mock?

A mock is a simulated object that mimics the behavior of a real component during testing. It allows the test to focus solely on the behavior of the unit under test without actually communicating with, for example, a database or a network.

The most popular tool for this is the Mockito framework:

1
2
MyService service = mock(MyService.class);
when(service.calculate()).thenReturn(42);

JUnit 5 assert methods

Assert methods verify whether the tested code behaves as expected. If the expected result does not match the actual one, the test fails.

Method Description
assertTrue(condition) / assertFalse(condition) Tests whether a logical condition is true/false
assertEquals(expected, actual) Compares expected and actual values
assertNotEquals(unexpected, actual) Checks inequality
assertNull(obj) / assertNotNull(obj) Checks for null state
assertSame(expected, actual) / assertNotSame(...) Checks if both references point to the same object
assertArrayEquals(expected[], actual[]) Compares elements of two arrays
assertIterableEquals(expected, actual) Compares two Iterable structures
assertThrows(Exception.class, executable) Verifies that an exception is thrown
assertTimeout(Duration.ofMillis(1000), executable) Verifies a time limit
fail("error message") Intentional failure

Defining test cases

JUnit uses annotations to define tests and control test life-cycle behavior:

Annotation Description
@Test Marks a method as a unit test
@ParameterizedTest + @ValueSource Test running with multiple inputs
@RepeatedTest Repeated test execution
@DisplayName Human-readable name in reports
@BeforeEach / @AfterEach Runs before/after every test
@BeforeAll / @AfterAll Runs once at the start/end of the test class
@Tag Tagging, e.g., slow, integration tests
@Disabled Temporarily disables a test

JUnit example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.ValueSource;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    class JUnit5Test {
        @ParameterizedTest
        @ValueSource(strings = { "cali", "bali", "dani" })
    void endsWithI(String str) {
          assertTrue(str.endsWith("i"));
     }
    }

Running tests

From an IDE (e.g., Eclipse, IntelliJ):

  • Right-click the test class → Run As → JUnit Test
  • JUnit results appear color-coded (green = passed, red = failed)

From the command line (Maven):

1
mvn test

With Gradle:

1
gradle test

In CI/CD systems (e.g., GitHub Actions, Jenkins):

  • Automatic execution as mvn test or gradle test steps.
  • Results can be exported in JUnit XML format for reporting.

xUnit examples

The following examples check whether the given string ends with the letter “i”.

C++

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ends_with_i_test.cpp
#include <gtest/gtest.h>
#include <string>

static bool ends_with_i(const std::string& s) {
    return !s.empty() && s.back() == 'i';
}

class EndsWithITest : public ::testing::TestWithParam<const char*> {};

TEST_P(EndsWithITest, EndsWithI) {
    std::string s = GetParam();
    EXPECT_TRUE(ends_with_i(s));
}

INSTANTIATE_TEST_SUITE_P(SampleInputs, EndsWithITest,
    ::testing::Values("cali", "bali", "dani"));

Running the above test:

1
2
3
4
5
# Example with CMake
# In CMakeLists.txt: find_package(GTest REQUIRED) ... etc.
cmake -S . -B build
cmake --build build
ctest --test-dir build

Python

1
2
3
4
5
6
# test_ends_with_i.py
import pytest

@pytest.mark.parametrize("s", ["cali", "bali", "dani"])
def test_ends_with_i(s):
    assert s.endswith("i")

Running the code:

1
2
pip install pytest
pytest -q

TypeScript (Jest)

1
2
3
4
5
6
// ends-with-i.test.ts
describe("endsWithI", () => {
  test.each(["cali", "bali", "dani"])('"%s" ends with i', (s: string) => {
    expect(s.endsWith("i")).toBe(true);
  });
});

Run:

1
2
3
4
5
npm init -y
npm i -D typescript ts-node jest ts-jest @types/jest
npx ts-jest config:init
# In package.json: "test": "jest"
npm test

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

@DisplayName("Parameterized example: does a word end with 'i'")
class EndsWithITest {

    @ParameterizedTest(name = "\"{0}\" ends with the letter i")
    @ValueSource(strings = { "cali", "bali", "dani" })
    void testEndsWithI(String input) {
        assertTrue(input.endsWith("i"),
                () -> "The string \"" + input + "\" does not end with 'i'!");
    }
}

Run:

1
2
3
mvn test
# or
gradle test

Mini Bookstore

Download the following project and unzip it! The project was created with IntelliJ IDEA Community Edition, which can be downloaded for free from this page.

Our task is to test the mini bookstore application. We have a small “bookstore” module with five main classes:

  • Product (abstract base class)
  • Book (simple business logic; e.g., long read)
  • PricingService (pricing)
  • InventoryRepository (inventory management)
  • CheckoutService (integrator/facade): uses both PricingService and InventoryRepository

User story 1: As a customer, I want the basic data of books (ID, name, price, author, page count) to be stored correctly so that correct information appears in the cart and on the invoice.

Acceptance criteria:

  • The constructor preserves all parameters; throws an exception for invalid parameters.
  • isLongRead() returns true exactly when page count > 300.
  • getCategory() always returns "BOOK".

User story 2: As a customer, I want the system to indicate if a book is a “long read” (> 300 pages) so that I can choose more easily.

Acceptance criteria:

  • calculateDiscountedPrice(price, percent) correctly computes for a percentage in [0..100].
  • clampPrice(price) converts negative values to 0.
  • isEligibleForLoyaltyDiscount(years) works correctly.

User story 3: As a loyal customer, I want the discount (percentage) and VAT to be calculated correctly so that I see a correct gross total.

Acceptance criteria:

  • addStock, hasStock, reserve handle inventory correctly; throw an exception on shortage.

User story 4: As a customer, I want the system to allow ordering only if there is sufficient stock, and to calculate the final total based on the ordered quantity, so that I don’t receive an incorrect receipt and stock does not go negative.

Acceptance criteria:

  • previewTotal(product, qty, discountPercent):
    • Checks stock.
    • After discount + VAT, multiplies by quantity and returns the preview total.
  • canFulfillOrder(product, qty): true/false based on stock
  • placeOrder(product, qty, discountPercent):
    • Reserves stock correctly

Task 1 – Writing unit tests:

Write at least 2 unit tests for the PricingService.addVat(netPrice) method, one of which explicitly exposes that it calculates with 25% instead of 27%.

Guidelines:

  • Choose a concrete net value (e.g., 10,000.0).
  • Expected gross: net * 1.27 = 12,700.0.
  • Write a second test as well (e.g., with another net amount or the edge case 0.0) so it’s not sensitive to just one case.
  • Do not modify the source code: the goal is to reveal bugs via tests.

Acceptance, if:

  • The test runs and fails on the current implementation (i.e., it actually detects the bug).
    • What is the bug?
  • After fixing the bug, it turns green.

Task 2 – Integration test: quantity multiplication for final total (CheckoutService)

Write an integration test for the CheckoutService.placeOrder(product, qty, discountPercent) method that demonstrates the missing multiplication by qty in the final total calculation.

Guidelines:

  • Create instances of InventoryRepository, PricingService, CheckoutService;
    • add stock for a Book product (e.g., 10 pcs).
  • Choose a quantity qty >= 2, and some discount (e.g., 10%).
  • Compute the gross unit price using the current PricingService functions:
    • perUnitGross = addVat(calculateDiscountedPrice(basePrice, discountPercent)).
  • Expected correct final total: qty * perUnitGross.
    • Is there a bug? If yes, what is it?
  • Check whether the "total=" value on the receipt is correct.

Additional checks:

  • Inventory decreases after placeOrder.
  • Throws an exception if stock is insufficient.

Acceptance, if:

  • The test fails on the current code (reveals the bug),
  • and turns green after fixing placeOrder.

Utolsó frissítés: 2025-10-09 14:29:54