Wednesday, September 20, 2023

Difference between @RunWith vs @ExtendWith Annotations in JUnit - Java Unit Testing Example

Hello guys, if you have written any unit test in Java then you must have come across JUnit and Mockito. JUnit is the most popular and widely-used testing framework in the Java ecosystem, enabling developers to write and execute unit tests for their Java applications. While writing tests, JUnit provides various annotations to facilitate different functionalities. Two of these annotations, @RunWith and @ExtendWith, play a crucial role in customizing the test execution process. In this article, we will explore the key differences between these two annotations and illustrate their usage through a Java program. This one is also one of the popular JUnit Interview question which I have also included in my earlier article about 20 Most asked JUnit Interview Questions for Java developers, if you haven't read it yet then you can also read it to learn more about JUnit and unit testing in Java. 


What is @RunWith Annotation in JUnit? What does it do?

The @RunWith annotation in JUnit allows developers to change the test runner, which is responsible for executing test cases. The default runner used by JUnit is the BlockJUnit4ClassRunner. However, there might be situations where you want to customize the test execution behavior, such as using a third-party test runner or a custom runner.

Example:

Let's assume we have a custom test runner named "CustomTestRunner" that performs some additional setup before executing the test cases. Here's how you can use the @RunWith annotation:

import org.junit.Test;

import org.junit.runner.RunWith;

import org.junit.runners.JUnit4;



@RunWith(CustomTestRunner.class)

public class MyCustomTest {



    @Test

    public void testExample() {

        // Test logic goes here

    }

}


The test cases inside the class "MyCustomTest" will be executed using the "CustomTestRunner" instead of the default JUnit4 runner.


What is @ExtendWith Annotation in JUnit? What does it do?

The @ExtendWith annotation aims to serve a different purpose compared to the @RunWith annotation. Instead of changing the test runner, @ExtendWith allows developers to register extensions, which can intercept the test execution lifecycle to add additional behavior or services.

Example:

Let's say we have a custom extension named "CustomExtension" that logs test execution information before and after each test method execution. Here's how you can use the @ExtendWith annotation:

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;



@ExtendWith(CustomExtension.class)

public class MyCustomTest {



    @Test

    public void testExample() {

        // Test logic goes here

    }

}

Here, the "CustomExtension" will be invoked before and after the execution of the test method "testExample," providing additional functionality without changing the test runner.


Difference between @RunWith and @ExtendWith Annotations in JUnit 5



Difference between @RunWith and @ExtendWith Annotations in JUnit 5

Now that you have understood what are @RunWith and @ExtendWith annotation in JUnit 5 and what does it do, is easy to find out differences between them. Following are the key differences:

1. Purpose

The @RunWith is used to change the test runner used for executing test cases in JUnit 4, on the other hand, @ExtendWith: Registers extensions that can intercept the test execution process in JUnit 5.


2. JUnit Version

The @RunWith is used and available in JUnit 4, while @ExtendWith is only available in JUnit 5 (Jupiter) onwards. 


3. Applicability

 The @RunWith annotation is applicable to classes in JUnit 4 to change the test runner, but @ExtendWith is annotation is also applicable to methods and classes in JUnit 5 to register extensions.

To demonstrate the difference between the @RunWith and @ExtendWith annotations in JUnit, let's create a Java program with test classes that showcase the usage of both annotations. We will use JUnit 4 for the @RunWith example and transition to JUnit 5 for the @ExtendWith example.


Ensure you have JUnit 4 and JUnit 5 dependencies added to your project. For Maven, include the following dependencies in your pom.xml file:

<!-- JUnit 4 -->

<dependency>

    <groupId>junit</groupId>

    <artifactId>junit</artifactId>

    <version>4.13.2</version>

    <scope>test</scope>

</dependency>



<!-- JUnit 5 -->

<dependency>

    <groupId>org.junit.jupiter</groupId>

    <artifactId>junit-jupiter-api</artifactId>

    <version>5.8.0</version>

    <scope>test</scope>

</dependency>


Assume we have a simple `Car` class representing a car's features and functionalities. We'll write test cases for the `Car` class, and in each case, we'll log some messages to simulate test executions.


Car class:

public class Car {

    private String make;

    private String model;

    private int year;



    // Constructor, getters, and setters go here

}

Next, let's write the test classes using both JUnit 4 and JUnit 5, demonstrating the `@RunWith` and `@ExtendWith` annotations, respectively.




JUnit 4 Test Class

import org.junit.Test;

import org.junit.runner.RunWith;



@RunWith(CustomJUnit4CarTestRunner.class)

public class JUnit4CarTest {



    @Test

    public void testCarMake() {

        Car car = new Car();

        car.setMake("Toyota");

        System.out.println("JUnit 4 - Testing car make: " + car.getMake());

    }



    @Test

    public void testCarYear() {

        Car car = new Car();

        car.setYear(2022);

        System.out.println("JUnit 4 - Testing car year: " + car.getYear());

    }

}


JUnit 5 Test Class


import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;



@ExtendWith(CustomJUnit5CarExtension.class)

public class JUnit5CarTest {



    @Test

    public void testCarMake() {

        Car car = new Car();

        car.setMake("Ford");

        System.out.println("JUnit 5 - Testing car make: " + car.getMake());

    }



    @Test

    public void testCarYear() {

        Car car = new Car();

        car.setYear(2023);

        System.out.println("JUnit 5 - Testing car year: " + car.getYear());

    }

}


Now, let's define custom runners and extensions for JUnit 4 and JUnit 5, respectively.  


Custom JUnit 4 Test Runner


import org.junit.runner.Description;

import org.junit.runner.RunWith;

import org.junit.runner.notification.RunNotifier;

import org.junit.runners.BlockJUnit4ClassRunner;



public class CustomJUnit4CarTestRunner extends BlockJUnit4ClassRunner {



    public CustomJUnit4CarTestRunner(Class<?> testClass)
      throws org.junit.runners.model.InitializationError {

        super(testClass);

    }



    @Override

    public void run(RunNotifier notifier) {

        System.out.println("JUnit 4 
        - Running tests for Car using CustomJUnit4CarTestRunner...");

        super.run(notifier);

        System.out.println("JUnit 4 - Finished running tests 
          for Car using CustomJUnit4CarTestRunner.");

    }



    @Override

    protected Description 
describeChild(org.junit.runners.model.FrameworkMethod method) {

        return 
Description.createTestDescription(getTestClass().getJavaClass(),

                "JUnit 4 - " + method.getName(), method.getAnnotations());

    }

}



Custom JUnit 5 Extension

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;


import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;

import org.junit.jupiter.api.extension.ExtensionContext;



public class CustomJUnit5CarExtension 
  implements BeforeTestExecutionCallback, AfterTestExecutionCallback {



    @Override

    public void beforeTestExecution(ExtensionContext context) {

        System.out.println("JUnit 5 - Before executing test: " 
+ context.getDisplayName());

    }



    @Override

    public void afterTestExecution(ExtensionContext context) {

        System.out.println("JUnit 5 - After executing test: " 
+ context.getDisplayName());

    }

}


Finally, we'll create a test runner class to execute both sets of test classes:


import org.junit.runner.JUnitCore;

import org.junit.runner.Result;

import org.junit.runner.notification.Failure;



public class CarTestRunner {



    public static void main(String[] args) {

        System.out.println("===== Running JUnit 4 
Car Tests with CustomJUnit4CarTestRunner =====");

        Result junit4Result = JUnitCore.runClasses(JUnit4CarTest.class);



        for (Failure failure : junit4Result.getFailures()) {

            System.out.println(failure.toString());

        }



        System.out.println("JUnit 4 Test Result: " 
+ junit4Result.wasSuccessful());

        System.out.println("=================
==============================================\n");



        System.out.println("===== Running JUnit 5 
Car Tests with CustomJUnit5CarExtension =====");

        Result junit5Result = JUnitCore.runClasses(JUnit5CarTest.class);



        for (Failure failure : junit5Result.getFailures()) {

            System.out.println(failure.toString());

        }



        System.out.println("JUnit 5 Test Result: " 
+ junit5Result.wasSuccessful());

        System.out.println("======================
=========================================\n");

    }

}


Upon running the `CarTestRunner` class, you will see the output demonstrating the difference between JUnit 4 and JUnit 5 and how each handles the test executions:


===== Running JUnit 4 Car Tests with CustomJUnit4CarTestRunner =====

JUnit 4 - Running tests for Car using CustomJUnit4CarTestRunner...

JUnit 4 - Testing car make: Toyota

JUnit 4 - Testing car year: 2022

JUnit 4 - Finished running tests for Car using CustomJUnit4CarTestRunner.

JUnit 4 Test Result: true

===============================================================



===== Running JUnit 5 Car Tests with CustomJUnit5CarExtension =====

JUnit 5 - Before executing test: testCarMake()

JUnit 5 - Testing car make: Ford

JUnit 5 - After executing test: testCarMake()

JUnit 5 - Before executing test: testCarYear()

JUnit 5 - Testing car year: 2023

JUnit 5 - After executing test: testCarYear()

JUnit 5 Test Result: true

===============================================================


In summary, this program demonstrates how JUnit 4 and JUnit 5 handle test execution for a real-world entity (a `Car` class) using custom runners and extensions, respectively. The output clearly shows the differences in the execution process and how each framework handles tests for the `Car` class.


Conclusion

In summary, both @RunWith and @ExtendWith annotations are crucial in customizing the test execution process in JUnit. The @RunWith annotation enables the use of custom test runners in JUnit 4, while the @ExtendWith annotation allows the registration of extensions in JUnit 5 to add additional behavior to the test execution lifecycle. Understanding the distinctions between these annotations is essential for effective unit testing in Java projects.


1 comment:

  1. Which one is recommended to use with JUnit 5? RunWith or ExtendWith?

    ReplyDelete