Home/Testing

Software Testing

Learn why testing is important and master JUnit 5 for Java testing.At the end of this page, you will find a navigation to mocking

Why Testing is Important

Imagine you're a chef making a cake

Testing in software is like tasting your cake before serving it to guests! **The Cake Story:** Imagine you're baking a chocolate cake for your friend's birthday. Would you just put it in a box and give it to them without checking if it tastes good? Of course not! You would:

Taste a small piece to see if it's sweet enough

Check if it's cooked properly in the middle

Make sure the frosting looks nice

Verify you didn't forget any ingredients

**Software Testing is the Same:** When programmers write code, they need to "taste" it (test it) before giving it to users. Testing helps us:

Find Bugs Early: Like finding out you forgot sugar before serving the cake

Save Time: Fixing a small mistake now is easier than fixing a big disaster later

Build Confidence: You know your code works correctly

Save Money: Bugs found in production cost 100x more to fix!

Make Users Happy: Nobody likes broken apps

**Real Example:** Remember when your favorite game crashed and you lost your progress? That probably happened because the developers didn't test that part of the game well enough!

Types of Testing

Different ways to test your code

Just like checking different parts of a cake, we test different parts of software: **1. Unit Testing** 🧪

Tests individual pieces of code (like testing just the frosting)

Fast and easy to write

Runs thousands of tests in seconds

**2. Integration Testing** 🔗

Tests how pieces work together (like checking if frosting sticks to cake)

Makes sure different parts of your app communicate correctly

**3. End-to-End Testing** 🎯

Tests the whole system (like serving and eating the whole cake)

Simulates real user behavior

**Focus: Unit Testing with JUnit** We'll focus on Unit Testing because:

It's the foundation of all testing

You write it while coding

It catches bugs immediately

It's required in most software jobs

JUnit 5 - The Testing Superhero

Modern Java testing made easy

JUnit 5 is like a helpful robot that checks your code for you! It's the most popular testing framework for Java. **Why JUnit 5?**

Latest version (released 2017, actively maintained)

More powerful than JUnit 4

Better support for modern Java features

Used by companies like Google, Netflix, Amazon

Step 1: Setup JUnit 5

Add JUnit 5 to your project (Maven):

xml
1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>

Step 2: Your First Test

Let's test a simple Calculator class:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Calculator.java - The class we want to test
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Cannot divide by zero!");
}
return a / b;
}
}

Now let's write tests:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test // This tells JUnit: "Hey, this is a test!"
void testAddition() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result, "2 + 3 should equal 5");
}
@Test
void testSubtraction() {
Calculator calc = new Calculator();
assertEquals(2, calc.subtract(5, 3));
}
@Test
void testMultiplication() {
Calculator calc = new Calculator();
assertEquals(6, calc.multiply(2, 3));
}
@Test
void testDivision() {
Calculator calc = new Calculator();
assertEquals(2, calc.divide(6, 3));
}
@Test
void testDivisionByZero() {
Calculator calc = new Calculator();
// This test expects an exception!
assertThrows(IllegalArgumentException.class, () -> {
calc.divide(10, 0);
});
}
}

Step 3: Important Annotations

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll // Runs ONCE before all tests
static void setupAll() {
System.out.println("Setting up test class...");
}
@BeforeEach // Runs BEFORE each test
void setupEach() {
System.out.println("Starting a test...");
}
@Test
void test1() {
System.out.println("Running test 1");
}
@Test
void test2() {
System.out.println("Running test 2");
}
@AfterEach // Runs AFTER each test
void cleanupEach() {
System.out.println("Finished a test...");
}
@AfterAll // Runs ONCE after all tests
static void cleanupAll() {
System.out.println("Cleaning up test class...");
}
@Disabled("Not ready yet") // Skip this test
@Test
void testNotReady() {
// This won't run
}
}

🏷️ What are Annotations?

Annotations are like sticky notes you put on your code! They start with @ and tell JUnit what to do. Think of them as instructions to your helpful robot assistant.

@Test

🎯 This is like raising your hand and saying 'Hey JUnit, this is a test method!' Without @Test, JUnit will just ignore your method.

@BeforeEach

🔄 Imagine washing your hands before eating each cookie. @BeforeEach runs before EVERY test to prepare things. Perfect for creating fresh test data!

@AfterEach

🧹 Like cleaning up your toys after playing! @AfterEach runs after every test to clean up. It closes files, clears memory, etc.

@BeforeAll

🏁 Like setting up the game board before a tournament. Runs ONCE before all tests start. Use it for expensive setup like database connections. Must be static!

@AfterAll

🏁 Like packing up the game when everyone goes home. Runs ONCE after all tests finish. Perfect for closing database connections. Must be static!

@Disabled

⏸️ Like putting a 'Do Not Disturb' sign on your test. JUnit will skip this test. Use it when a test is broken or you're not ready yet. Don't forget to remove it later!

💡 Think of it Like a School Day:

  • @BeforeAll: Opening the school building (once)
  • @BeforeEach: Starting each class (every period)
  • @Test: The actual lesson
  • @AfterEach: Cleaning the classroom after each class
  • @AfterAll: Closing the school building (once)

Step 4: Common Assertions

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import static org.junit.jupiter.api.Assertions.*;
class AssertionsExampleTest {
@Test
void testAssertions() {
// Check if values are equal
assertEquals(4, 2 + 2);
assertEquals("Hello", "Hel" + "lo");
// Check if values are NOT equal
assertNotEquals(5, 2 + 2);
// Check if something is true/false
assertTrue(5 > 3);
assertFalse(5 < 3);
// Check if something is null/not null
String text = null;
assertNull(text);
text = "Hello";
assertNotNull(text);
// Check if same object
String a = "test";
String b = a;
assertSame(a, b);
// Check arrays
int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals(expected, actual);
// Group multiple assertions
assertAll("Person tests",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName()),
() -> assertEquals(30, person.getAge())
);
}
}

✅ What are Assertions?

Assertions are like a teacher checking your homework! They verify if your code gives the correct answer. If the assertion fails, the test fails and tells you what went wrong.

assertEquals(expected, actual)

🎯 The most popular assertion! It's like asking 'Are these two things the same?' Example: assertEquals(4, 2+2) checks if 2+2 really equals 4.

💡 Kid analogy: Like checking if you have the same number of cookies as your friend!

assertTrue(condition)

✅ Checks if something is true. Example: assertTrue(5 > 3) passes because 5 is indeed greater than 3!

💡 Kid analogy: Like checking if the door is really open before walking through!

assertFalse(condition)

❌ The opposite of assertTrue! Checks if something is false. Example: assertFalse(5 < 3) passes because 5 is NOT less than 3.

💡 Kid analogy: Making sure the monster is NOT under your bed!

assertNull(object)

🫙 Checks if something is null (empty/nothing). Example: assertNull(text) passes if text has no value.

💡 Kid analogy: Checking if your piggy bank is empty!

assertNotNull(object)

📦 The opposite! Checks if something is NOT null. Example: assertNotNull(user) makes sure the user object exists.

💡 Kid analogy: Making sure there's actually a toy in the box!

assertThrows(ExceptionType.class, code)

💥 Checks if your code throws an error when it should! Example: When dividing by zero, you EXPECT an error. This assertion makes sure that error happens.

💡 Kid analogy: Like expecting your mom to say 'No!' when you ask for ice cream before dinner!

assertAll(assertions...)

📋 Runs multiple checks together! Instead of stopping at the first failure, it runs ALL assertions and shows you everything that's wrong. Super helpful!

💡 Kid analogy: Like a teacher checking all your answers on a test, not just stopping at the first wrong one!

assertArrayEquals(expected, actual)

🔢 Special assertion for arrays! Checks if two arrays have the same values in the same order. Example: {1,2,3} equals {1,2,3} but NOT {3,2,1}.

💡 Kid analogy: Making sure two toy trains have the same colored cars in the same order!

🎓 Pro Tip: Custom Messages

You can add custom messages to make debugging easier:

assertEquals(5, calc.add(2, 3), "Adding 2+3 should equal 5!");

When the test fails, you'll see your message. It's like leaving notes for your future self!

Step 5: Parameterized Tests (Test Many Inputs)

Instead of writing many similar tests, use @ParameterizedTest:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ParameterizedTestExample {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testIsPositive(int number) {
assertTrue(number > 0);
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 5, 10",
"10, 20, 30"
})
void testAddition(int a, int b, int expected) {
Calculator calc = new Calculator();
assertEquals(expected, calc.add(a, b));
}
@ParameterizedTest
@MethodSource("provideStrings")
void testStringLength(String str, int expectedLength) {
assertEquals(expectedLength, str.length());
}
static Stream<Arguments> provideStrings() {
return Stream.of(
Arguments.of("Hello", 5),
Arguments.of("Test", 4),
Arguments.of("JUnit", 5)
);
}
}

Step 6: Best Practices

Remember: F.I.R.S.T Principles

  • Fast: Tests should run quickly
  • Independent: Each test should work alone
  • Repeatable: Same result every time
  • Self-validating: Pass or fail, no manual checking
  • Timely: Write tests with or before code

Test Naming Convention

java
1
2
3
4
5
6
7
8
9
// Good test names tell a story:
@Test
void whenDividingByZero_thenThrowsException() { }
@Test
void givenEmptyList_whenAddingItem_thenSizeIsOne() { }
@Test
void shouldReturnTrueWhenUserIsActive() { }

Real-World Example: User Service

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// UserService.java
public class UserService {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
users.add(user);
}
public User findByEmail(String email) {
return users.stream()
.filter(u -> u.getEmail().equals(email))
.findFirst()
.orElse(null);
}
public int getUserCount() {
return users.size();
}
public List<User> getActiveUsers() {
return users.stream()
.filter(User::isActive)
.collect(Collectors.toList());
}
}
// UserServiceTest.java
class UserServiceTest {
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService();
}
@Test
void whenAddingValidUser_thenUserIsAdded() {
User user = new User("john@example.com", "John", true);
userService.addUser(user);
assertEquals(1, userService.getUserCount());
assertNotNull(userService.findByEmail("john@example.com"));
}
@Test
void whenAddingNullUser_thenThrowsException() {
assertThrows(IllegalArgumentException.class, () -> {
userService.addUser(null);
});
}
@Test
void whenAddingUserWithInvalidEmail_thenThrowsException() {
User user = new User("invalid-email", "John", true);
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> userService.addUser(user)
);
assertTrue(exception.getMessage().contains("Invalid email"));
}
@Test
void whenGettingActiveUsers_thenReturnsOnlyActive() {
userService.addUser(new User("active@test.com", "Active", true));
userService.addUser(new User("inactive@test.com", "Inactive", false));
userService.addUser(new User("active2@test.com", "Active2", true));
List<User> activeUsers = userService.getActiveUsers();
assertEquals(2, activeUsers.size());
assertTrue(activeUsers.stream().allMatch(User::isActive));
}
@ParameterizedTest
@CsvSource({
"test@example.com, true",
"invalid-email, false",
"missing-at-symbol.com, false",
"user@domain.com, true"
})
void testEmailValidation(String email, boolean shouldBeValid) {
User user = new User(email, "Test User", true);
if (shouldBeValid) {
assertDoesNotThrow(() -> userService.addUser(user));
} else {
assertThrows(IllegalArgumentException.class,
() -> userService.addUser(user));
}
}
}

🎭 Next: Learn About Mocking!

Now that you understand testing basics, learn how to use MOCK objects to test your code without real databases or APIs. It's like using toy phones instead of real phones!

Learn Mocking with Mockito

Key Takeaways

Testing is Like Insurance

You hope you never need it, but you're glad you have it when something goes wrong!

Start Small

Don't try to test everything at once. Start with the most important functions!