DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Design Patterns for Scalable Test Automation Frameworks
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • Implementing Exponential Backoff With Spring Retry

Trending

  • Cosmos DB Disaster Recovery: Multi-Region Write Pitfalls and How to Evade Them
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  • Docker Base Images Demystified: A Practical Guide
  • Scaling DevOps With NGINX Caching: Reducing Latency and Backend Load
  1. DZone
  2. Coding
  3. Frameworks
  4. Enhancing Spring Test Framework with beforeClass and afterClass setup

Enhancing Spring Test Framework with beforeClass and afterClass setup

How to allow instance methods to run as JUnit BeforeClass behavior.

By 
Zemian Deng user avatar
Zemian Deng
·
Oct. 11, 12 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
36.7K Views

Join the DZone community and get the full member experience.

Join For Free

JUnit allows you to setup methods on the class level once before and after all tests methods invocation. However, by design on purpose that they restrict this to only

package deng.junitdemo;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class DemoTest {

    @Test
    public void testOne() {
        System.out.println("Normal test method #1.");
    }

    @Test
    public void testTwo() {
        System.out.println("Normal test method #2.");
    }

    @BeforeClass
    public static void beforeClassSetup() {
        System.out.println("A static method setup before class.");
    }

    @AfterClass
    public static void afterClassSetup() {
        System.out.println("A static method setup after class.");
    }
}

And above should result the following output:

A static method setup before class.
Normal test method #1.
Normal test method #2.
A static method setup after class.

This usage is fine for most of the time, but there are times you want to use non-static methods to setup the test. I will show you a more detailed use case later, but for now, let's see how we can solve this naughty problem with JUnit first. We can solve this by making the test implements a Listener that provide the before and after callbacks, and we will need to digg into JUnit to detect this Listener to invoke our methods. This is a solution I came up with:

package deng.junitdemo;

import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(InstanceTestClassRunner.class)
public class Demo2Test implements InstanceTestClassListener {

    @Test
    public void testOne() {
        System.out.println("Normal test method #1");
    }

    @Test
    public void testTwo() {
        System.out.println("Normal test method #2");
    }

    @Override
    public void beforeClassSetup() {
        System.out.println("An instance method setup before class.");
    }

    @Override
    public void afterClassSetup() {
        System.out.println("An instance method setup after class.");
    }
}

As stated above, our Listener is a simple contract:

package deng.junitdemo;

public interface InstanceTestClassListener {
    void beforeClassSetup();
    void afterClassSetup();
}

Our next task is to provide the JUnit runner implementation that will trigger the setup methods.

package deng.junitdemo;

import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

public class InstanceTestClassRunner extends BlockJUnit4ClassRunner {

    private InstanceTestClassListener InstanceSetupListener;

    public InstanceTestClassRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected Object createTest() throws Exception {
        Object test = super.createTest();
        // Note that JUnit4 will call this createTest() multiple times for each
        // test method, so we need to ensure to call "beforeClassSetup" only once.
        if (test instanceof InstanceTestClassListener && InstanceSetupListener == null) {
            InstanceSetupListener = (InstanceTestClassListener) test;
            InstanceSetupListener.beforeClassSetup();
        }
        return test;
    }

    @Override
    public void run(RunNotifier notifier) {
        super.run(notifier);
        if (InstanceSetupListener != null)
            InstanceSetupListener.afterClassSetup();
    }
}

Now we are in business. If we run above test, it should give us similar result, but this time we are using instance methods instead!

An instance method setup before class.
Normal test method #1
Normal test method #2
An instance method setup after class.

A concrete use case: Working with Spring Test Framework

Now let me show you a real use case with above. If you use Spring Test Framework, you would normally setup a test like this so that you may have test fixture injected as member instance.

package deng.junitdemo.spring;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.List;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringDemoTest {

    @Resource(name="myList")
    private List<String> myList;

    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }
}

You would also need a spring xml under that same package for above to run:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
     <bean id="myList" class="java.util.ArrayList">
        <constructor-arg>
            <list>
                <value>one</value>
                <value>two</value>
            </list>
        </constructor-arg>
     </bean>
</beans>

Pay very close attention to member instance List<String> myList. When running JUnit test, that field will be injected by Spring, and it can be used in any test method. However, if you ever want a one time setup of some code and get a reference to a Spring injected field, then you are in bad luck. This is because the JUnit @BeforeClass will force your method to be static; and if you make your field static, Spring injection won't work in your test!

Now if you are a frequent Spring user, you should know that Spring Test Framework already provided a way for you to handle this type of use case. Here is a way for you to do class level setup with Spring's style:

package deng.junitdemo.spring;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.List;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners(listeners = {
        DependencyInjectionTestExecutionListener.class, 
        SpringDemo2Test.class})
@ContextConfiguration
public class SpringDemo2Test extends AbstractTestExecutionListener {

    @Resource(name="myList")
    private List<String> myList;

    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }

    @Override
    public void afterTestClass(TestContext testContext) {
        List<?> list = testContext.getApplicationContext().getBean("myList", List.class);
        assertThat((String)list.get(0), is("one"));
    }

    @Override
    public void beforeTestClass(TestContext testContext) {
        List<?> list = testContext.getApplicationContext().getBean("myList", List.class);
        assertThat((String)list.get(1), is("two"));
    }
}

As you can see, Spring offers the @TestExecutionListeners annotation to allow you to write any Listener, and in it you will have a reference to the TestContext which has the ApplicationContext for you to get to the injected field reference. This works, but I find it not very elegant. It forces you to look up the bean, while your injected field is already available as field. But you can't use it unless you go through the TestContext parameter.

Now if you mix the solution we provided in the beginning, we will see a more prettier test setup. Let's see it:

package deng.junitdemo.spring;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.List;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;

import deng.junitdemo.InstanceTestClassListener;

@RunWith(SpringInstanceTestClassRunner.class)
@ContextConfiguration
public class SpringDemo3Test implements InstanceTestClassListener {

    @Resource(name="myList")
    private List<String> myList;

    @Test
    public void testMyListInjection() {
        assertThat(myList.size(), is(2));
    }

    @Override
    public void beforeClassSetup() {
        assertThat((String)myList.get(0), is("one"));
    }

    @Override
    public void afterClassSetup() {
        assertThat((String)myList.get(1), is("two"));
    }
}

Now JUnit only allow you to use single Runner, so we must extends the Spring's version to insert what we did before.

package deng.junitdemo.spring;

import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import deng.junitdemo.InstanceTestClassListener;

public class SpringInstanceTestClassRunner extends SpringJUnit4ClassRunner {

    private InstanceTestClassListener InstanceSetupListener;

    public SpringInstanceTestClassRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }

    @Override
    protected Object createTest() throws Exception {
        Object test = super.createTest();
        // Note that JUnit4 will call this createTest() multiple times for each
        // test method, so we need to ensure to call "beforeClassSetup" only once.
        if (test instanceof InstanceTestClassListener && InstanceSetupListener == null) {
            InstanceSetupListener = (InstanceTestClassListener) test;
            InstanceSetupListener.beforeClassSetup();
        }
        return test;
    }

    @Override
    public void run(RunNotifier notifier) {
        super.run(notifier);
        if (InstanceSetupListener != null)
            InstanceSetupListener.afterClassSetup();
    }
}

That should do the trick. Running the test will give use this output:

12:58:48 main INFO  org.springframework.test.context.support.AbstractContextLoader:139 | Detected default resource location "classpath:/deng/junitdemo/spring/SpringDemo3Test-context.xml" for test class [deng.junitdemo.spring.SpringDemo3Test].
12:58:48 main INFO  org.springframework.test.context.support.DelegatingSmartContextLoader:148 | GenericXmlContextLoader detected default locations for context configuration [ContextConfigurationAttributes@74b23210 declaringClass = 'deng.junitdemo.spring.SpringDemo3Test', locations = '{classpath:/deng/junitdemo/spring/SpringDemo3Test-context.xml}', classes = '{}', inheritLocations = true, contextLoaderClass = 'org.springframework.test.context.ContextLoader'].
12:58:48 main INFO  org.springframework.test.context.support.AnnotationConfigContextLoader:150 | Could not detect default configuration classes for test class [deng.junitdemo.spring.SpringDemo3Test]: SpringDemo3Test does not declare any static, non-private, non-final, inner classes annotated with @Configuration.
12:58:48 main INFO  org.springframework.test.context.TestContextManager:185 | @TestExecutionListeners is not present for class [class deng.junitdemo.spring.SpringDemo3Test]: using defaults.
12:58:48 main INFO  org.springframework.beans.factory.xml.XmlBeanDefinitionReader:315 | Loading XML bean definitions from class path resource [deng/junitdemo/spring/SpringDemo3Test-context.xml]
12:58:48 main INFO  org.springframework.context.support.GenericApplicationContext:500 | Refreshing org.springframework.context.support.GenericApplicationContext@44c9d92c: startup date [Sat Sep 29 12:58:48 EDT 2012]; root of context hierarchy
12:58:49 main INFO  org.springframework.beans.factory.support.DefaultListableBeanFactory:581 | Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@73c6641: defining beans [myList,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy
12:58:49 Thread-1 INFO  org.springframework.context.support.GenericApplicationContext:1025 | Closing org.springframework.context.support.GenericApplicationContext@44c9d92c: startup date [Sat Sep 29 12:58:48 EDT 2012]; root of context hierarchy
12:58:49 Thread-1 INFO  org.springframework.beans.factory.support.DefaultListableBeanFactory:433 | Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@73c6641: defining beans [myList,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; root of factory hierarchy

Obviously the output shows nothing interesting here, but the test should run with all assertion passed. The point is that now we have a more elegant way to invoking a before and after test setup that are at class level, and they can be instance methods to allow Spring injection.

Download the demo code

You may get above demo code in a working Maven project from my sandbox.

Testing Spring Framework Framework

Published at DZone with permission of Zemian Deng, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Design Patterns for Scalable Test Automation Frameworks
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • Implementing Exponential Backoff With Spring Retry

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!