Recommended resources
The freely available resources this lesson will direct you to.
Introduction
In our last lesson, you mastered the art of designing test cases using Equivalence Partitioning and Boundary Value Analysis. You now have a systematic method for answering the crucial question: "What should I test?" This gives you a strategic blueprint for ensuring quality.
Today, we move from blueprint to construction. This lesson is about translating those test case designs into executable code. You will learn how to write automated unit tests for both the backend and frontend of a modern web application. We'll cover the industry-standard frameworks for your Java and React stack: JUnit 5 and React Testing Library (RTL). By the end of this lesson, you will be able to write fundamental unit tests that verify the correctness of your code, providing a safety net for future changes and refactoring.
1. Unit Testing Your Java Backend with JUnit 5
As a Java developer, you've written many methods and classes. Unit testing is the practice of verifying that each of these individual "units" of code works as expected in isolation. For this, the Java ecosystem relies almost universally on JUnit. Given your experience with Spring Boot, you'll find that projects generated via Spring Initializr typically come with JUnit 5 pre-configured, making it easy to get started.
Let's begin with a clear, visual introduction to the core concepts of unit testing with JUnit.
Java Unit Testing with JUnit - Tutorial - How to Create And Use Unit Tests
The video "Java Unit Testing with JUnit" by Coding with John provides an excellent, step-by-step walkthrough of creating and running your first unit tests.
Please watch the following segments to get a solid foundation: What is Unit Testing? This part defines the purpose of unit testing and how to include JUnit in a Maven project. Your First Test: This segment is crucial. It demonstrates the entire basic workflow: creating a test class, using the @Test annotation, instantiating the class under test, and using assertEquals to check the result. This follows the classic "Arrange-Act-Assert" pattern. Running Tests & Reading Failures: Watch this section to see how to run tests in an IDE and, more importantly, how to interpret a test failure. Testing for Exceptions: Finally, watch from this point to learn how to use assertThrows. This is essential for verifying that your code correctly handles invalid inputs, a key part of robust software.
The Arrange-Act-Assert (AAA) Pattern
As you saw in the video, unit tests have a clean, predictable structure. This is often formalized as the AAA pattern:
- Arrange: Set up the test. This involves creating objects, preparing inputs, and defining the expected outcome.
- Act: Execute the method you are testing.
- Assert: Verify the outcome. This is where you check if the actual result matches the expected result.
Writing Our discountPercentage Tests
Let's apply this to the practical exercise from our last lesson. You designed test cases for a discountPercentage field that must be an integer between 10 and 90.
Imagine we have a PromotionValidator class in our Spring Boot application:
public class PromotionValidator {
public void validateDiscount(int discountPercentage) {
if (discountPercentage < 10 || discountPercentage > 90) {
throw new IllegalArgumentException("Discount must be between 10 and 90.");
}
}
}
Now, let's write the JUnit tests for it. A test class would look like this:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PromotionValidatorTest {
private PromotionValidator validator;
@BeforeEach
void setUp() {
// Arrange: This runs before each test, setting up a fresh validator instance.
validator = new PromotionValidator();
}
@Test
@DisplayName("Should pass for a valid discount percentage within the range")
void testValidDiscount() {
// Act & Assert: This test checks a valid case (e.g., 50).
// assertDoesNotThrow executes the code and asserts that no exception is thrown.
assertDoesNotThrow(() -> validator.validateDiscount(50));
}
@Test
@DisplayName("Should pass for a valid discount at the lower boundary")
void testLowerBoundaryDiscount() {
// Act & Assert: Testing the boundary value 10.
assertDoesNotThrow(() -> validator.validateDiscount(10));
}
@Test
@DisplayName("Should pass for a valid discount at the upper boundary")
void testUpperBoundaryDiscount() {
// Act & Assert: Testing the boundary value 90.
assertDoesNotThrow(() -> validator.validateDiscount(90));
}
@Test
@DisplayName("Should throw IllegalArgumentException for a discount below the lower boundary")
void testDiscountBelowBoundary() {
// Arrange: Define the expected exception type and message.
Class<IllegalArgumentException> expectedException = IllegalArgumentException.class;
String expectedMessage = "Discount must be between 10 and 90.";
// Act & Assert: Use assertThrows to verify the exception.
IllegalArgumentException exception = assertThrows(expectedException, () -> {
validator.validateDiscount(9); // Input from our boundary analysis
});
// Optionally, assert that the exception message is correct.
assertEquals(expectedMessage, exception.getMessage());
}
@Test
@DisplayName("Should throw IllegalArgumentException for a discount above the upper boundary")
void testDiscountAboveBoundary() {
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
validator.validateDiscount(91); // Input from our boundary analysis
});
}
}
This test class directly implements the test cases you designed. Notice the use of @DisplayName to make test reports more readable and @BeforeEach to avoid repetitive setup code. These are powerful features of JUnit 5 that help keep your tests clean and maintainable.
For a deeper dive into the rich set of assertions and annotations JUnit 5 offers, the official Vogella tutorial is an invaluable resource.
JUnit 5 tutorial - Learn how to write unit tests
This article by vogella is a comprehensive guide to JUnit 5. It details the various assertions and features available.
Please focus on these two sections: Read the section titled "Example: Writing comprehensive JUnit 5 tests". It reinforces the concepts from the video with a Calculator class example, showing @DisplayName, @RepeatedTest, and testing for exceptions. Skim the section "Assertions and assumptions". Pay attention to the table of basic assertions and the assertAll example, which is useful when you want to check multiple properties of an object at once.
2. Unit Testing Your React Frontend with React Testing Library (RTL)
On the frontend, the philosophy of testing is slightly different. While we still test "units" (components), the focus is less on internal logic and more on the user's experience. React Testing Library (RTL) is built around this principle: "The more your tests resemble the way your software is used, the more confidence they can give you."
Instead of testing a component's internal state or its instance methods, you'll render the component and interact with it just as a user would: by finding elements on the screen and triggering events like clicks or typing.
This video from Programming with Mosh is a fantastic introduction to this mindset and the mechanics of RTL.
React Testing for Beginners: Start Here!
This "React Testing for Beginners" video provides a practical, hands-on guide to getting started with React Testing Library and user-event.
Please watch the following key segments: What to Test: This section is critical. It explains the philosophy of testing behavior over implementation details. First Test Example: Watch from here to see how to write your first component test. Focus on the core loop: render the component, use screen.getByRole to find elements, and use expect(...).toBeInTheDocument() to make assertions. Testing User Interactions: The segment from this timestamp introduces the user-event library, which is essential for simulating clicks and other user actions realistically. Exercise Walkthrough: Watch the ExpandableText exercise. It's a perfect example of testing a component that changes based on user interaction (clicking a "Show More" / "Show Less" button).
The RTL Workflow: Render, Query, Interact, Assert
The process for testing a React component with RTL is:
- Render: Use the
renderfunction from RTL to render your component into a virtual DOM. - Query: Use methods on the
screenobject (likegetByRole,getByText,getByLabelText) to find elements. The best practice is to query elements the way a user would find them (e.g., by their accessible role or visible text). - Interact (Optional): Use the
user-eventlibrary to simulate user actions like clicking a button (userEvent.click(...)) or typing into an input (userEvent.type(...)). - Assert: Use
expectwith matchers from thejest-domlibrary (liketoBeInTheDocument,toBeEnabled,toHaveTextContent) to verify the result.
Writing a Test for a <DiscountForm /> Component
Let's imagine a simple React component for our discount feature.
// DiscountForm.jsx
import React, { useState } from 'react';
export const DiscountForm = ({ onSubmit }) => {
const [discount, setDiscount] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
setDiscount(e.target.value);
};
const handleSubmit = () => {
const discountValue = parseInt(discount, 10);
if (discountValue >= 10 && discountValue <= 90) {
setError('');
onSubmit(discountValue);
} else {
setError('Discount must be between 10 and 90.');
}
};
return (
<div>
<h1>Set Promotion</h1>
{error && <p role="alert">{error}</p>}
<label htmlFor="discount">Discount Percentage</label>
<input
id="discount"
type="number"
value={discount}
onChange={handleChange}
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
};
Here is how you would test it using RTL and user-event.
// DiscountForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DiscountForm } from './DiscountForm';
// In a real project, you would import 'vi' from 'vitest' or 'jest' from '@jest/globals'
const mockSubmit = vi.fn(); // or jest.fn()
describe('DiscountForm', () => {
beforeEach(() => {
// Clear mock history before each test
mockSubmit.mockClear();
});
test('should render the form with input and button', () => {
// Arrange & Act
render(<DiscountForm onSubmit={mockSubmit} />);
// Assert
expect(screen.getByRole('heading', { name: /set promotion/i })).toBeInTheDocument();
expect(screen.getByLabelText(/discount percentage/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();
});
test('should show an error message for an invalid discount', async () => {
const user = userEvent.setup();
// Arrange
render(<DiscountForm onSubmit={mockSubmit} />);
// Act
const input = screen.getByLabelText(/discount percentage/i);
await user.type(input, '5'); // Invalid value
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert
expect(screen.getByRole('alert')).toHaveTextContent('Discount must be between 10 and 90.');
expect(mockSubmit).not.toHaveBeenCalled();
});
test('should not show an error message initially', () => {
// Arrange
render(<DiscountForm onSubmit={mockSubmit} />);
// Assert
// Use queryBy... to check for an element that should NOT be there.
// getBy... throws an error if not found, queryBy... returns null.
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
test('should call onSubmit with the correct value for a valid discount', async () => {
const user = userEvent.setup();
// Arrange
render(<DiscountForm onSubmit={mockSubmit} />);
// Act
const input = screen.getByLabelText(/discount percentage/i);
await user.type(input, '75'); // Valid value
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(mockSubmit).toHaveBeenCalledWith(75);
expect(mockSubmit).toHaveBeenCalledTimes(1);
});
});
This example shows how to test initial rendering, user input, conditional error messages (queryByRole), and callback functions.
For a comprehensive text-based guide that covers the different query types (getBy, queryBy, findBy) and other advanced topics in great detail, this tutorial by Robin Wieruch is an excellent read.
React Testing Library Tutorial
This article is a thorough tutorial covering the core concepts and practical applications of React Testing Library.
I recommend reading through the following sections to solidify your understanding: Start with the introduction to understand how RTL fits into the testing ecosystem. Read the sections on rendering and selecting elements. The section on search variants is key. It explains the crucial difference between getBy, queryBy, and findBy. Finally, read about simulating user interactions with fireEvent and userEvent and how to test callback handlers.
Practical Application
Your task is to apply these concepts to your current work. This will create an immediate "paper trail of competence" and build the habit of writing tests.
-
Backend (Java/JUnit):
- Find a simple, pure function in your Spring Boot application. A "pure function" is one that for a given input, always returns the same output and has no side effects (like database calls or network requests). A good candidate might be a method in a utility class or a simple calculation within a service.
- Write one unit test that verifies its correct behavior for a typical input using
assertEquals. - If the method has input validation, write a second test that verifies it throws the correct exception for an invalid input, using
assertThrows.
-
Frontend (React/RTL):
- Identify a simple presentational component in your React application.
- Write one test to verify that it renders correctly. Assert that a key piece of text or an important element (like a button) is in the document.
- If the component has a simple interaction (e.g., a button that calls a function passed via props), write a second test. Simulate a user click with
user-eventand assert that the mocked prop function was called.
This exercise is not about achieving 100% test coverage today. It's about breaking the seal and writing your first two automated tests, creating a foundation you will build upon throughout your career.
Conclusion
You have now moved from designing test cases to implementing them in code. This is a foundational skill for any senior developer, as it ensures code quality, prevents regressions, and enables confident refactoring.
Key Takeaways:
- The Arrange-Act-Assert pattern provides a clear structure for all unit tests.
- JUnit 5 is the standard for Java. You use the
@Testannotation to define tests and assertion methods likeassertEqualsandassertThrowsto verify behavior. - React Testing Library focuses on testing user-facing behavior. The core workflow is to
rendera component,queryfor elements usingscreen, simulate interactions withuser-event, andassertthe outcome. - Use
getBy...to find elements you expect to be present, andqueryBy...to verify an element is absent.
In our next lesson, we will move up the testing pyramid from unit to integration testing. You will learn how to write integration tests for Spring Boot REST API endpoints, a crucial step in verifying that different parts of your backend service work together correctly. We will also introduce mocking with Mockito to isolate our tests from external dependencies like the database.