انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-045-L03

أولى اختبارات JUnit 5

  • الدرس 3 من 7
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

You now know what a test case looks like. Writing our own Check.equal helper and our own runner would get tiresome, and every team would invent a different one. JUnit 5 is the standard Java library that does this job: it finds your test methods, runs each one separately, collects the failures, and shows a green or red result in your IDE.

In this lesson you will add JUnit 5 to a project, write a test class with @Test methods, use the main assertions (assertEquals, assertTrue, assertFalse, assertNull), learn the rule about argument order, and run the tests from the IDE.

This lesson is hands-on. Menu names differ between IDEs and versions, so the setup steps below describe what to look for rather than promising exact clicks. All the code was compiled and run with JDK 21 and JUnit 5.12.2.

It builds directly on lesson 2: every test here follows Arrange, Act, Assert, gets a behavior name, and creates its own objects.

Learning Objectives

By the end of this lesson you will be able to:

  • Add JUnit 5 to a plain Java project through your IDE.
  • Write a test class and mark test methods with @Test.
  • Use assertEquals, assertTrue, assertFalse, and assertNull correctly.
  • Put the expected value first and the actual value second, and explain why it matters.
  • Compare double values with a tolerance (a delta).
  • Run one test or a whole class from the IDE and tell green from red.

Written Explanation

Adding JUnit to the project

Until the build-tool module, your project has no build tool, so the IDE adds the library for you. In IntelliJ IDEA, the usual path is: write a test class, and when the IDE notices that the testing library is missing it offers to add it (look for a suggestion on the @Test annotation, often reachable with Alt+Enter). Alternatively, open the Project Structure dialog, go to Libraries, and add a library from Maven by name. JetBrains documents both routes; menu names and shortcuts may differ in your version or in another IDE, so follow your tool's own help if they do not match.

Choose the JUnit 5 library (its Maven coordinates start with org.junit.jupiter). Be careful not to pick an older "JUnit 4" option: the annotations live in a different package and some names differ. In a later module a build tool (Maven) will manage this for you, and your projects will be migrated.

A typical project layout keeps the code and its tests in separate folders, with the test class named after the class it tests:

text
src/Account.java
test/AccountTest.java

If your IDE offers a "Tests" source root, use it. The two files are compiled together, so the test can use Account directly.

A test class

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

class CalculatorTest {
    @Test
    void addsTwoNumbers() {
        assertEquals(5, new Calculator().add(2, 3));
    }
}

The pieces:

  • @Test (from org.junit.jupiter.api) marks a method as a test. JUnit finds every @Test method and runs it.
  • A test method returns void and takes no parameters. According to the JUnit user guide, test methods and classes can have any access level except private, and the guide recommends leaving out public unless you have a reason. That is why the examples here have no public.
  • import static org.junit.jupiter.api.Assertions.*; lets you write assertEquals(...) instead of Assertions.assertEquals(...). The static import is the same feature as Math.max becoming max.
  • JUnit creates a new instance of the test class for each test method by default, which helps keep tests independent. Do not rely on fields to pass data from one test to another.

JUnit decides the order in which test methods run, and it is not necessarily the order you wrote them in. In our run, maxOfEqualNumbersIsThatNumber ran before maxReturnsLarger even though it was written after. Your tests must pass in any order, which lesson 2 already required.

The assertions you need first

All of these are static methods of org.junit.jupiter.api.Assertions.

AssertionPasses when
assertEquals(expected, actual)the two values are equal
assertEquals(expected, actual, delta)two double values differ by no more than delta
assertTrue(condition)the condition is true
assertFalse(condition)the condition is false
assertNull(value)the value is null
assertNotNull(value)the value is not null

When an assertion fails, it throws an AssertionFailedError; JUnit catches it and marks that test as failed, then carries on with the other tests.

Prefer assertEquals over assertTrue(a == b): when it fails it prints both values, while assertTrue can only say "expected true". Use assertTrue and assertFalse for conditions that really are yes/no, such as account.isActive(). For objects, assertEquals uses equals, so it works for String and for your own classes once they implement equals correctly (the equals/hashCode lesson).

Expected first, actual second

The signature is assertEquals(expected, actual); the JUnit Javadoc names the first two parameters expected and actual in that order. The failure message prints them under those labels, so if you swap the arguments, the message lies to you. Example 3 shows it.

Comparing doubles

Account stores a double. Decimal fractions such as 0.1 are stored approximately in binary, so arithmetic can give a result that differs in the last digits. For double, give a tolerance:

java
assertEquals(0.3, 0.1 + 0.2, 0.0001);

The third argument is the delta: the largest difference you accept. Whole numbers like 150.0 are exactly representable, so the plain two-argument form is fine for them, but a delta is a safe habit for money-like values in these early examples. (Real systems usually avoid double for money; that is a design topic for later.)

Running tests

In IntelliJ IDEA, a run icon appears in the gutter next to the class name and next to each test method. Clicking the icon next to a method runs that one test; the icon next to the class runs all its tests. Results appear in a Run window: a green mark for each passed test and a red mark for each failure. Other IDEs have similar icons or a "Run test" command. Green everywhere means every test passed; any red means something needs attention, which lesson 6 covers.

Examples

Example 1: the first test (simple)

The Calculator from the lecture, with an add method and a static max:

java
class CalculatorTest {
    @Test
    void addsTwoNumbers() {
        assertEquals(5, new Calculator().add(2, 3));
    }

    @Test
    void maxReturnsLarger() {
        assertEquals(7, Calculator.max(3, 7));
    }

    @Test
    void maxOfEqualNumbersIsThatNumber() {
        assertEquals(4, Calculator.max(4, 4));
    }
}

Run result (from our small runner; an IDE shows the same results as a tree with green marks):

text
SUCCESSFUL  addsTwoNumbers()
SUCCESSFUL  maxOfEqualNumbersIsThatNumber()
SUCCESSFUL  maxReturnsLarger()

Note the order, which differs from the order in the file. Each test also fits on a few lines: one Arrange-Act-Assert, one behavior.

Example 2: testing Account (practical)

java
class AccountTest {
    @Test
    void depositIncreasesBalance() {
        Account account = new Account("Sam", 100);
        account.deposit(50);
        assertEquals(150, account.getBalance());
    }

    @Test
    void newAccountKeepsOpeningBalance() {
        Account account = new Account("Sam", 100);
        assertEquals(100.0, account.getBalance(), 0.001);
    }

    @Test
    void zeroBalanceIsAllowed() {
        Account account = new Account("Sam", 0);
        assertTrue(account.getBalance() == 0);
        assertFalse(account.getBalance() > 0);
    }
}

All three passed (tests: 3, passed: 3, failed: 0). The first calls assertEquals(150, ...) with an int and a double: Java widens the int, so the double version is used. The second uses a delta, the third shows assertTrue and assertFalse for yes/no conditions.

Example 3: doubles and swapped arguments (realistic)

Two traps, with real output from JUnit 5.12.2.

java
@Test
void exactComparisonOfSums() {
    assertEquals(0.3, 0.1 + 0.2);
}
text
FAILED  exactComparisonOfSums()
   org.opentest4j.AssertionFailedError: expected: <0.3> but was: <0.30000000000000004>

Adding the delta (0.0001) made the same check pass. The code did nothing wrong here; the problem is how decimal fractions are represented, so the test must allow for it.

Now swap the arguments in a test of an account that holds 100 where we (wrongly) expect 150:

java
assertEquals(account.getBalance(), 150);   // swapped
assertEquals(150, account.getBalance());   // correct order
text
swapped:  expected: <100.0> but was: <150.0>
correct:  expected: <150.0> but was: <100.0>

Both tests fail, because the balance really is 100. But the swapped version claims the code returned 150 when it actually returned 100. Anyone reading that message will look for the bug in the wrong place. Expected first, actual second, every time.

Backend Insight

JUnit 5 tests are plain Java classes, so they run the same way in the IDE and on a build server. Later, Maven will run every @Test method in the project with a single command, and the same suite will run before code is merged. Writing tests in the standard format now means they carry over unchanged.

In a backend, the assertions you will use most are exactly these: assertEquals on values and fields, assertTrue/assertFalse on flags, assertNull for "not found" results. The next lesson adds the one assertion that checks failures: assertThrows.

Common Mistakes

Mistake 1: Swapping expected and actual

  • Incorrect: assertEquals(account.getBalance(), 150).
  • Correct: assertEquals(150, account.getBalance()).

The test still passes or fails correctly, but the failure message is backwards.

Mistake 2: Forgetting @Test

A method without @Test is just a method; JUnit ignores it, and nothing in the IDE warns you that a check never ran. If you add a test and the count of tests does not go up, check the annotation.

Mistake 3: Importing the wrong JUnit

Importing org.junit.Test (JUnit 4) instead of org.junit.jupiter.api.Test can give confusing results. Use the jupiter package for JUnit 5.

Mistake 4: Using assertTrue for everything

  • Incorrect: assertTrue(account.getBalance() == 150).
  • Correct: assertEquals(150, account.getBalance()), which shows the actual value on failure.

Exercises

Level A: Foundation

A1. Which annotation marks a JUnit 5 test method, and from which package is it imported?

أظهر الحل A1

A1. @Test, imported from org.junit.jupiter.api.

A2. For assertEquals(a, b), which argument should be the value the code produced?

أظهر الحل A2

A2. The second one. The first is the expected value.

A3. Which assertion would you use to check that findNickname("Sam") returned nothing (null)?

أظهر الحل A3

A3. assertNull.

Level B: Application

B1. Write a JUnit 5 test class CalculatorTest with two tests for Calculator.add: one for positive numbers and one involving a negative number.

أظهر الحل B1

B1.

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

class CalculatorTest {
    @Test
    void addsTwoPositiveNumbers() {
        assertEquals(5, new Calculator().add(2, 3));
    }

    @Test
    void addsNegativeAndPositive() {
        assertEquals(-1, new Calculator().add(-4, 3));
    }
}

We ran a version of this class against the Calculator shown above and it passed.

B2. Write a test that a new Account with opening balance 80 has balance 80, using a delta.

أظهر الحل B2

B2.

java
@Test
void newAccountHasOpeningBalance() {
    Account account = new Account("Ana", 80);
    assertEquals(80.0, account.getBalance(), 0.001);
}

B3. A test fails with expected: <10> but was: <8>. Which value did the code return, and what would the message say if the arguments had been swapped?

أظهر الحل B3

B3. The code returned 8 (the "actual" value). With swapped arguments the message would read expected: <8> but was: <10>, which wrongly suggests the code returned 10.

Level C: Challenge

C1. Write three tests for Calculator.max that together cover: the first argument larger, the second larger, and both equal. Name them by behavior. Then explain why one more test with negative numbers adds value.

أظهر الحل C1

C1.

java
@Test void maxReturnsFirstWhenFirstIsLarger() { assertEquals(9, Calculator.max(9, 2)); }
@Test void maxReturnsSecondWhenSecondIsLarger() { assertEquals(7, Calculator.max(3, 7)); }
@Test void maxOfEqualNumbersIsThatNumber()      { assertEquals(4, Calculator.max(4, 4)); }

A negative-number test such as max(-5, -2) would catch implementations that accidentally assume non-negative values (for example, one that starts a running maximum at 0), a bug the first three tests would miss.

C2. A teammate writes this test and says it passes, so deposit must be correct:

java
@Test
void deposit() {
    Account account = new Account("Sam", 100);
    account.deposit(50);
    assertTrue(account.getBalance() > 0);
}

Find two problems and rewrite the test.

أظهر الحل C2

C2. Problems: the name deposit does not state a behavior; and balance > 0 would be true even if deposit did nothing (100 is already above 0), so the test cannot detect most bugs. Better:

java
@Test
void depositIncreasesBalanceByTheAmount() {
    Account account = new Account("Sam", 100);
    account.deposit(50);
    assertEquals(150, account.getBalance());
}

Mini Challenge

In your IDE, create a new test class for a small class you wrote earlier (a calculator, a temperature converter, a word counter). Write at least four @Test methods, with behavior names, each in Arrange, Act, Assert form, using at least three different assertion methods. Run the whole class, then run one test alone. Finally, break the code under test on purpose and confirm that at least one test turns red.

Check your work: the number of tests the IDE reports matches the number of @Test methods, no test uses an object created by another test, and for each failing message you can say which side (expected or actual) is the code's answer.

Summary & Next Lesson

Key takeaways

  • JUnit 5 is the standard Java testing library; add it through your IDE for now (a build tool comes later).
  • Mark each test with @Test; test methods return void, take no parameters, and are not private.
  • Use assertEquals(expected, actual), with expected first. Add a delta for double.
  • Use assertTrue, assertFalse, assertNull, and assertNotNull for yes/no and null checks.
  • JUnit chooses the test order and gives each test a new instance, so tests must stand alone.

Next lesson

The next lesson, "Testing exceptions", shows how to prove that code rejects bad input with assertThrows.

Self-check

  1. What do you import to use @Test in JUnit 5?
  2. Why does swapping expected and actual matter if the test still fails correctly?
  3. Why is assertEquals(0.3, 0.1 + 0.2) risky?
  4. A new test is added but the IDE's test count does not go up. What is the likely cause?
  5. In what order do JUnit tests run?
أظهر الإجابات
  1. org.junit.jupiter.api.Test.
  2. The failure message labels the values, so a swap makes it describe the bug backwards.
  3. Decimal fractions are stored approximately, so the sum is 0.30000000000000004; use a delta.
  4. The @Test annotation is missing (or the wrong JUnit package was imported).
  5. In an order chosen by JUnit that you should not rely on; every test must pass in any order.

References

  • JUnit documentation, Writing Tests: Test Classes and Methods. Supports that test classes and methods may have any access level except private, that the guide recommends omitting public, and that they cannot return a value or be abstract.
  • JUnit documentation, Writing Tests: Assertions. Its examples put the expected value first, as in assertEquals(2, calculator.add(1, 1)).
  • JUnit 5 Assertions Javadoc. Supports that the first two parameters of assertEquals are expected and actual and that delta is a non-negative tolerance.
  • JUnit documentation, Test Instance Lifecycle. Supports the per-method new instance default and its purpose.
  • JUnit documentation, Test Execution Order. Supports that the default order is deterministic but not obvious.
  • JetBrains, IntelliJ IDEA: JUnit. Supports the IDE steps: the IDE can offer to add a missing testing library, Project Structure | Libraries can add one from Maven, and a gutter run icon runs one test or a class. Menus may differ by version.
  • All code and output were compiled and run on JDK 21 with JUnit 5.12.2 through a small launcher program, not through an IDE, so IDE screens are described from the JetBrains page and not from my own session. Failure-message text may differ slightly between JUnit versions.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.