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
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
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
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):
<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:
// Calculator.java - The class we want to testpublic 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:
// CalculatorTest.javaimport 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
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
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:
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
// Good test names tell a story:@Testvoid whenDividingByZero_thenThrowsException() { }@Testvoid givenEmptyList_whenAddingItem_thenSizeIsOne() { }@Testvoid shouldReturnTrueWhenUserIsActive() { }Real-World Example: User Service
// UserService.javapublic 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.javaclass 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 MockitoKey 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!