Computer scienceProgramming languagesJavaInterview preparationTech interviewJava fundamentals and OOP

Testing tech interview

36 minutes read

Testing is a critical skill for software developers, ensuring code quality and reliability. This guide covers essential concepts in unit testing, the JUnit framework, assertion types, and mock testing with Mockito, providing a strong foundation for interview preparation.

Introduction to Testing

Testing is an integral part of software development, aimed at verifying that code behaves as expected. It helps identify bugs and ensures that code changes do not introduce regressions.

Advantages of Testing:

  • Improved Code Quality: Regular testing can catch bugs early in the development process.

  • Facilitates Refactoring: Ensures that code changes do not break existing functionality.

  • Documentation: Tests can serve as documentation for how the code is supposed to work.

Unit Testing

Unit testing focuses on testing individual units or components of a software. JUnit is a popular framework for writing and running tests.

Benefits of Unit Testing:

  • Isolation: Tests are written for individual components in isolation.

  • Speed: Unit tests are typically fast to write and execute.

Cons:

  • Limited Scope: Does not catch integration issues between components.

JUnit Framework

JUnit is a widely used framework for unit testing in Java and Kotlin. It provides annotations to identify test methods and assertions to verify expected outcomes.

Basic Annotations:

  • @Test: Marks a method as a test method.

  • @BeforeEach: Code to run before each test.

  • @AfterEach: Code to run after each test.

  • @BeforeAll and @AfterAll: Code to run once before/after all tests.

Assertions in JUnit:

Assertions are used to validate the expected outcome of a test.

  • assertEquals(expected, actual): Checks if two values are equal.

  • assertNotNull(actual): Checks if a value is not null.

  • assertTrue(condition): Checks if a condition is true.

  • assertAll: Group multiple assertions.

  • assertThrows: Verify that a specific exception is thrown.

Example with a Calculator:

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@DisplayName("Calculator Test Suite")
class CalculatorTest {
    private val calculator = Calculator()

    @Test
    @DisplayName("Addition should return the correct result")
    fun testAddition() {
        assertEquals(5, calculator.add(2, 3))
    }

    @Test
    @DisplayName("Division by zero should throw an exception")
    fun testDivisionByZero() {
        assertThrows<ArithmeticException> {
            calculator.divide(5, 0)
        }
    }

    @Test
    @DisplayName("Multiple operations should be correct")
    fun testMultipleOperations() {
        assertAll(
            { assertEquals(5, calculator.add(2, 3)) },
            { assertEquals(1, calculator.subtract(3, 2)) },
            { assertEquals(6, calculator.multiply(2, 3)) }
        )
    }
}
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

@DisplayName("Calculator Test Suite")
class CalculatorTest {
    private final Calculator calculator = new Calculator();

    @Test
    @DisplayName("Addition should return the correct result")
    void testAddition() {
        assertEquals(5, calculator.add(2, 3));
    }

    @Test
    @DisplayName("Division by zero should throw an exception")
    void testDivisionByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(5, 0);
        });
    }

    @Test
    @DisplayName("Multiple operations should be correct")
    void testMultipleOperations() {
        assertAll(
                () -> assertEquals(5, calculator.add(2, 3)),
                () -> assertEquals(1, calculator.subtract(3, 2)),
                () -> assertEquals(6, calculator.multiply(2, 3))
        );
    }
}

Mockito for Mocking

Mockito is a powerful framework for creating mock objects, which are essential for isolating components during testing. By using mocks, you can simulate the behavior of complex objects and focus on testing the logic of the class under test.

Stubs vs. Mocks:

  • Stubs: Stubs are used to provide predefined responses to method calls made during tests. They are mainly used to define the behavior of a dependency without any concern for how those methods are called. Stubs help to control the environment in which the class is tested by supplying consistent and expected outcomes. However, they do not verify interactions with the dependency.

    Example of Stub:

    // Stub for consistent return value
    whenever(calculator.add(2, 3)).thenReturn(5)
    // Stub for consistent return value
    when(calculator.add(2, 3)).thenReturn(5)
  • Mocks: Mocks, on the other hand, are full-featured replacements of objects that can both define behavior and verify interactions. They are designed to confirm that certain methods are called with specified parameters and are instrumental in interaction testing. While stubs focus on controlling data returned by the calls, mocks capture the interaction history with the object.

    Example of Mock:

    // Verify interaction with methods
    verify(calculator).multiply(100.0, 0.05)

Dependency Injection

Dependency injection is a design pattern that allows you to replace real dependencies with mock objects during testing. This approach is crucial for ensuring that your tests are isolated and not dependent on external systems or services.

Annotations

  • @Mock: This annotation is used to create a mock object.

  • @InjectMocks: This annotation is used to inject mock objects into the class being tested. It helps in setting up the test environment by automatically injecting the mocks into the fields of the class under test.

Using Mockito

Below is an example of how to use Mockito in a test class:

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.DisplayName
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.jupiter.MockitoExtension
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockitoExtension::class)
@DisplayName("InterestCalculator Test")
class InterestCalculatorTest {
    @Mock
    lateinit var calculator: Calculator

    @InjectMocks
    lateinit var interestCalculator: InterestCalculator

    @Test
    @DisplayName("Calculate Interest should use calculator to compute interest")
    fun calculateInterestShouldUseCalculatorToComputeInterest() {
        whenever(calculator.multiply(100.0, 0.05)).thenReturn(5.0)

        val interest = interestCalculator.calculateInterest(100.0, 0.05)

        assertEquals(5.0, interest)
        verify(calculator).multiply(100.0, 0.05)
    }

    @Test
    @DisplayName("Log Transaction should not throw any exception")
    fun logTransactionShouldNotThrowException() {
        doNothing().whenever(calculator).logTransaction(anyString())

        interestCalculator.logTransaction("Test transaction")

        verify(calculator).logTransaction("Test transaction")
    }

    @Test
    @DisplayName("Divide by zero should throw ArithmeticException")
    fun divideByZeroShouldThrowArithmeticException() {
        doThrow(ArithmeticException("Division by zero")).whenever(calculator).divide(anyDouble(), eq(0.0))

        assertThrows<ArithmeticException> {
            interestCalculator.divide(10.0, 0.0)
        }

        verify(calculator).divide(10.0, 0.0)
    }
}
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("InterestCalculator Test")
class InterestCalculatorTest {
    @Mock
    private Calculator calculator;

    @InjectMocks
    private InterestCalculator interestCalculator;

    @Test
    @DisplayName("Calculate Interest should use calculator to compute interest")
    void calculateInterestShouldUseCalculatorToComputeInterest() {
        when(calculator.multiply(100.0, 0.05)).thenReturn(5.0);

        double interest = interestCalculator.calculateInterest(100.0, 0.05);

        assertEquals(5.0, interest);
        verify(calculator).multiply(100.0, 0.05);
    }

    @Test
    @DisplayName("Log Transaction should not throw any exception")
    void logTransactionShouldNotThrowException() {
        doNothing().when(calculator).logTransaction(anyString());

        interestCalculator.logTransaction("Test transaction");

        verify(calculator).logTransaction("Test transaction");
    }

    @Test
    @DisplayName("Divide by zero should throw ArithmeticException")
    void divideByZeroShouldThrowArithmeticException() {
        doThrow(new ArithmeticException("Division by zero")).when(calculator).divide(anyDouble(), eq(0.0));

        assertThrows(ArithmeticException.class, () -> {
            interestCalculator.divide(10.0, 0.0);
        });

        verify(calculator).divide(10.0, 0.0);
    }
}

Key Mockito Methods

  • Programming Behaviors:

  • whenever (Kotlin) | when (Java): Used to define the return value of a mock method call.

    whenever(calculator.add(2, 3)).thenReturn(5)
    when(calculator.add(2, 3)).thenReturn(5);
  • doNothing: Used when you want a void method to do nothing upon invocation.

    doNothing().whenever(calculator).reset()
    doNothing().when(calculator).reset();
  • doThrow: Used to simulate an exception being thrown by a method.

    doThrow(RuntimeException::class).whenever(calculator).divide(anyDouble(), eq(0.0))
    doThrow(RuntimeException.class).when(calculator).divide(anyDouble(), eq(0.0));
  • verify: This method checks if a particular method of a mock object was called with specific arguments. It is useful for ensuring that the expected interactions with the mock object occur. Using verify is important because it ensures that your code interacts with its dependencies as expected, which is a critical aspect of testing the behavior of your application.

    verify(calculator).multiply(100.0, 0.05)

Spring Test

In addition to unit testing individual components, it's crucial to test the integration of these components with the Spring framework's functionalities. Spring provides several testing tools to help verify the interactions with real databases, RESTful endpoints, and more.

@DataJpaTest

@DataJpaTest is a specialized test annotation provided by Spring Boot designed to test JPA repositories. It configures an in-memory database, Hibernate, and Spring Data for testing data access logic.

  • Purpose: To focus on JPA components without loading the entire application context.

  • Configuration: Automatically configures Hibernate, Spring Data JPA, and an in-memory database.

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Optional: to use a real database
class UserRepositoryTest {
    @Autowired
    lateinit var userRepository: UserRepository

    @Test
    fun `should find user by username`() {
        val user = userRepository.save(User(username = "johndoe", email = "[email protected]"))
        val foundUser = userRepository.findByUsername("johndoe")
        assertEquals(user.id, foundUser?.id)
    }
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.*;

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Optional: to use a real database
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldFindUserByUsername() {
        User user = userRepository.save(new User("johndoe", "[email protected]"));
        User foundUser = userRepository.findByUsername("johndoe");
        assertNotNull(foundUser);
        assertEquals(user.getId(), foundUser.getId());
    }
}

WebTestClient

WebTestClient is used for testing WebFlux applications but can also be used with RESTful services via WebClient. It is a non-blocking, reactive client for testing web servers.

  • Purpose: To test HTTP requests and validate the responses in WebFlux applications.

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.reactive.server.WebTestClient
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
import org.springframework.web.reactive.function.BodyInserters

@SpringJUnitConfig(classes = [WebFluxApplication::class])
class UserRestTest {
    @Autowired
    lateinit var webTestClient: WebTestClient

    @Test
    fun `test get all users`() {
        webTestClient.get().uri("/users")
            .exchange()
            .expectStatus().isOk
            .expectBodyList(User::class.java)
            .hasSize(5) // Example validation
    }
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(classes = WebFluxApplication.class)
class UserRestTest {
    @Autowired
    private WebTestClient webTestClient;

    @Test
    void testGetAllUsers() {
        webTestClient.get().uri("/users")
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(User.class)
                .hasSize(5); // Example validation
    }
}

MockMvc

MockMvc is a powerful tool for testing Spring MVC applications. It allows you to perform requests against a mocked servlet environment.

  • Purpose: To test controllers' interactions with the Spring MVC stack without starting the server.

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get

@WebMvcTest(UserController::class)
class UserControllerTest {
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun `should return all users`() {
        mockMvc.get("/users")
            .andExpect {
                status { isOk() }
                content { contentType("application/json") }
            }
    }
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
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(UserController.class)
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldReturnAllUsers() throws Exception {
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("application/json"));
    }
}

RestTemplate

RestTemplate is a synchronous client to perform HTTP requests for Spring applications, typically used for testing purposes in areas not involving WebFlux or when typical unit testing doesn't suffice.

  • Purpose: To perform integration tests involving HTTP requests on standard Spring MVC applications.

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.http.HttpStatus

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestTemplateTest {
    @LocalServerPort
    private var port: Int = 0

    @Autowired
    lateinit var restTemplate: TestRestTemplate

    @Test
    fun `should return all users over HTTP`() {
        val url = "http://localhost:$port/users"
        val response = restTemplate.getForEntity(url, Array<User>::class.java)
        assertEquals(HttpStatus.OK, response.statusCode)
        assertEquals(5, response.body?.size) // Example assertion
    }
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestTemplateTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnAllUsersOverHttp() {
        String url = "http://localhost:" + port + "/users";
        ResponseEntity<User[]> response = restTemplate.getForEntity(url, User[].class);

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertEquals(5, response.getBody().length); // Example assertion
    }
}

Incorporating these tools allows developers to effectively test different layers of their applications, ensuring robust integration with Spring components.

Test-Driven Development (TDD)

TDD is a software development approach where tests are written before the code. It follows the cycle:

  1. Red: Write a failing test.

  2. Green: Write the minimum code to pass the test.

  3. Refactor: Improve the code while ensuring tests still pass.

    The example will demonstrate a simple function to calculate the sum of two numbers.

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

// Step 1: Red - Write a failing test
class CalculatorTest {
    @Test
    @DisplayName("Should return the sum of two numbers")
    fun testAddition() {
        val calculator = Calculator()
        val result = calculator.add(2, 3)
        assertEquals(5, result)
    }
}

// Step 2: Green - Write the minimum code to pass the test
class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
}

// Step 3: Refactor - Improve the code while ensuring tests still pass
// In this simple example, there might not be much to refactor, but
// the process encourages reviewing and improving the code structure.
// Step 1: Red - Write a failing test
class CalculatorTest {
    @Test
    @DisplayName("Should return the sum of two numbers")
    void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }
}

// Step 2: Green - Write the minimum code to pass the test
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// Step 3: Refactor - Improve the code while ensuring tests still pass
// In this simple example, there might not be much to refactor, but
// the process encourages reviewing and improving the code structure.

Explanation:

  1. Red: We start by writing a test case in CalculatorTest that checks if the add function returns the correct sum of two numbers. Initially, this test will fail because the Calculator class and add function do not exist.

  2. Green: We then implement the Calculator class and the add function to return the sum of two numbers, ensuring the test passes.

  3. Refactor: Finally, we review the code to see if any improvements can be made while keeping all tests passing. In this simple case, the code is already quite straightforward.

Importance of TDD:

  • Encourages simple designs and inspires confidence to refactor.

  • Ensures comprehensive test coverage.

Final Thoughts

By mastering these testing techniques and tools, developers can ensure high-quality software and be well-prepared for technical interviews. Regular practice can enhance your skills and confidence. Good luck with your interviews!

How did you like the theory?
Report a typo