← Back to list

Unit test — part1

What is Unit Testing?

Rakesh raj · 2024-12-21 11:28 · 0 claps · 4.1 min read
#unit-testing #junit #junit-5 #mockito
Open on Medium ↗

Unit test — part1

What is Unit Testing?

  • Definition: A unit test is a small, isolated test that focuses on a specific unit of code, typically a single method or function, to ensure it behaves as expected.
  • Purpose: While unit tests are not designed to uncover broad application-wide issues or regressions, they:
  • Ensure that individual units of code work as expected.
  • Provide “living documentation” that can be executed, reducing the need for verbose comments or JavaDoc.
  • Act as a safety net, catching bugs early in the development process.
  • Quote: “Unit tests are not designed to uncover application-wide bugs or regressions, but to ensure individual units of code work as expected.” — Steven Sanderson.

Characteristics of Good Unit Tests

  • Fast: Unit tests should run quickly to keep the development cycle efficient.
  • Isolated: Tests should be independent of one another, meaning the outcome of one test should not affect the others.
  • Repeatable: Unit tests should always produce the same result when executed multiple times.
  • Self-Validating: The results of a unit test should be easily verifiable without manual inspection.
  • Timely: Unit tests can be written at any time but are most effective when written before or during development (Test-Driven Development — TDD).

Unit Test Lifecycle (Arrange, Act, Assert)

Unit tests follow a standard Arrange-Act-Assert (AAA) pattern:

  1. Arrange: Set up the necessary preconditions and inputs for the test.
  2. Act: Call the method or function that you are testing.
  3. Assert: Verify that the output or behavior matches the expected result.

Variation: Some prefer using AAAA (Arrange, Act, Assert, After), where an After step is included to clean up or perform additional checks post-execution.

JUnit Annotations and Lifecycle

  • @Test: Marks a method as a test method.
  • @BeforeEach: Runs before each individual test method.
  • @AfterEach: Runs after each individual test method.
  • @BeforeAll: Runs once before all test methods (must be static).
  • @AfterAll: Runs once after all test methods (must be static).
  • assertEquals(expected, actual): Verifies that the expected and actual values are equal.
  • assertTrue(condition): Verifies that the condition is true.
  • assertFalse(condition): Verifies that the condition is false.
  • assertNull(object): Verifies that the object is null.
  • assertNotNull(object): Verifies that the object is not null.
  • assertThrows(Exception.class, () -> { }): Verifies that a specific exception is thrown during execution.

Best Practices for Writing Unit Tests

  • Mocking Expensive Operations: Mock external dependencies like database calls or HTTP requests to speed up tests. Use @BeforeAll for mocking in test setup to avoid repeating the same expensive operation in every test.
  • Avoid Unnecessary Try/Catch: Avoid cluttering tests with try/catch blocks for checked exceptions. Let the exception propagate naturally if it’s part of the test.
  • Descriptive Assertions: Use meaningful assertions rather than relying on comments. A comment like assertThat("account balance is 100", account.getBalance(), equalTo(50)) is misleading because the comment states an expectation of 100, which doesn't match the actual expected result (50).

Floating Point Comparison

When comparing floating-point numbers (e.g., float or double), always account for precision errors. Use a tolerance value to compare the numbers within a reasonable margin.

assertTrue(Math.abs((2.32 * 3) - 6.96) < 0.0005);

This avoids issues where floating-point precision causes small differences that fail assertions.

Avoiding Common Pitfalls

  • Test Order: JUnit runs tests in random order, so tests must be independent of each other. Do not assume the order in which they run.
  • Test Independence: If tests depend on each other, refactor to ensure that each test is self-contained. A failing test should not cause others to fail unless they share a common dependency or state.
  • Code Smell: If test setup becomes large or complex, it’s often a sign of a deeper design issue in the code. Try to refactor the code so that it is easier to test.

Test Naming Conventions

Descriptive Test Names: Use clear, descriptive test names that explain the scenario being tested. Follow the Given-When-Then pattern when naming your tests:

  • Given: Initial state or input
  • When: Action or method being tested
  • Then: Expected result

Example:

  • givenBalanceIsZero_whenDepositIsMade_thenBalanceIsUpdated

This helps anyone reading the test understand what is being tested without needing to read the implementation.

Testing Private Methods

  • Private Method Testing: The need to test private methods often signals that the class is too large and not well-designed. If a class has too many private methods, consider refactoring to move those private behaviors into separate classes where they can be made public and tested more easily.
  • Design Insight: A class with a large number of private methods usually violates the Single Responsibility Principle (SRP), indicating that the class may be trying to do too much. When private methods become too complex to test directly, they should often be extracted into separate classes with clear, well-defined responsibilities.
  • Refactor to Simpler Units: Instead of writing unit tests for private methods, refactor the code to create smaller, more focused classes with clear public interfaces that are easier to test.

The FIRST Principles

  • Fast: Unit tests should execute quickly to avoid delays in the development process.
  • Isolated: Each test should be independent. A failure in one test should not affect others.
  • Repeatable: The test should return the same result every time it runs.
  • Self-validating: The outcome should be automated and verifiable without human inspection.
  • Timely: Write tests at any point in the development process (ideally early, via TDD).

Test Coverage and Boundary Conditions

  • Boundary Testing: Ensure tests cover edge cases and boundary conditions, such as:
  • Invalid inputs (e.g., bad filenames, empty strings, null).
  • Data that exceeds reasonable limits (e.g., extremely high or low values, like a person’s age being 150 years).
  • Overflow/Underflow: Test for numeric overflows or underflows.
  • Null and Missing Values: Test for cases where values might be null, empty, or invalid.
  • Example Boundary Tests:
  • Null input
  • Empty input ("")
  • Input exceeding expected length/size
  • Values that might cause overflow (Integer.MAX_VALUE + 1)

Cross-checking and Error Handling

  • Cross-check with Alternative Methods: Use multiple ways to verify that the output is correct. For example, use a different method or algorithm to calculate the result and compare the outputs.
  • Error Handling: Test how the code behaves when errors occur. Ensure that your system handles edge cases gracefully (e.g., network failures, disk space issues, or system crashes).

Concurrency and Thread Safety

If your application involves multi-threading:

  • Ensure that your tests account for concurrency. For example, test that the application behaves as expected when multiple threads access shared resources simultaneously.
  • Synchronization: If your code involves shared state, ensure that it is thread-safe, using synchronization where needed.

메타데이터
post_id
3611b77fdb7d
slug
unit-test-part1-3611b77fdb7d
url
https://medium.com/@rrlinus5/unit-test-part1-3611b77fdb7d
canonical_url
https://medium.com/@rrlinus5/unit-test-part1-3611b77fdb7d
author_url
https://medium.com/@rrlinus5
status
ok
fetched_at
2026-06-27 07:40:21