简体   繁体   中英

How to test multiple Spring Boot applications in one test?

I have a multi-module Maven project with 2 Spring Boot applications

parent

  • fooApp
  • barApp
  • test

How to set up a test where you can load separate spring boot applications, each with its own configuration context, in the same process.

public abstract class AbstractIntegrationTest {//test module

    protected FOO foo;
    protected BAR bar;

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @IntegrationTest
    @Transactional
    @SpringApplicationConfiguration(classes = foo.Application.class)
    public class FOO {
        public MockMvc mockMvc;

        @Autowired
        public WebApplicationContext wac;

        @Before
        public void _0_setup() {
            MockitoAnnotations.initMocks(this);
            mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
            TestCase.assertNotNull(mockMvc);
        }

        public void login(String username) {
        }
    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @IntegrationTest
    @Transactional
    @SpringApplicationConfiguration(classes = bar.Application.class)
    public class BAR {

        @Autowired
        public WebApplicationContext wac;

        public MockMvc restMvc;

        @Before
        public void _0_setup() {
            MockitoAnnotations.initMocks(this);
            restMvc = MockMvcBuilders.webAppContextSetup(wac).build();
            TestCase.assertNotNull(restMvc);
        }

        public void login(String username) {
        }
    }

    @Before
    public void _0_setup() {
        foo = new FOO();
        bar = new BAR();
    }
}

And an example of an integration test

public class IntegrationTest extends AbstractIntegrationTest {

    @Test
    public void login() {
        foo.login("foologin");
        bar.login("barlogin");
    }

}

I agree with @rainerhahnekamp who said that what you are trying to achieve is more like a system/integration test.

However if you still want to do your test this way, I think it's still doable.

First, one important things to know :
Importing both fooApp and barApp project inside test project will make configuration files of both projects available to the classloader, and will produce impredictable results. Exemple : only one of the two application.properties file will be loaded. So you will have to use 2 differents profiles to load 2 separated configuration files setup.
For the same reason of projects files overlapping, beans defined by one application in packages visible by the other one will be loaded in both apps context.

To test the concept I created one service and one rest controller in each project, each with a 'profiled' property file :

barApp


@EnableAutoConfiguration(
    exclude = {SecurityAutoConfiguration.class,
    ManagementWebSecurityAutoConfiguration.class})
@SpringBootApplication
public class BarApp {


  public static void main(String[] args) {
    SpringApplication.run(BarApp.class, args);
  }

}

@Service
public class BarService {

  public String yield() {
    return "BarService !";
  }

}

@RestController
public class BarResource {

  private final BarService barService;

  public BarResource(BarService barService) {
    this.barService = barService;
  }

  @GetMapping("/bar")
  public String getBar() {
    return barService.yield();
  }

}

application-bar.properties :

server.port=8181

fooApp


@EnableConfigurationProperties
@SpringBootApplication
public class FooApp {
  
  public static void main(String[] args) {
    SpringApplication.run(FooApp.class, args);
  }

}

@Service
public class FooService {

  public String yield() {
    return "FooService !";
  }

}

@RestController
public class FooResource {

  private final FooService fooService;

  public FooResource(FooService fooService) {
    this.fooService = fooService;
  }

  @GetMapping("/foo")
  public String getFoo() {
    return fooService.yield();
  }

}

application-foo.properties :

server.port=8282

test

class TestApps {

  @Test
  void TestApps() {
    // starting and customizing BarApp
    {
      SpringApplication barApp = new SpringApplication(BarApp.class);
      barApp.setAdditionalProfiles("bar"); // to load 'application-bar.properties'
      GenericWebApplicationContext barAppContext = (GenericWebApplicationContext) barApp.run();

      BarService barServiceMock = Mockito.mock(BarService.class);
      Mockito.doReturn("mockified bar !").when(barServiceMock).yield();
      barAppContext.removeBeanDefinition("barService");
      barAppContext.registerBean("barService", BarService.class, () -> barServiceMock);
    }

    // starting and customizing FooApp
    {
      SpringApplication fooApp = new SpringApplication(FooApp.class);
      fooApp.setAdditionalProfiles("foo"); // to load 'application-foo.properties'
      GenericWebApplicationContext fooAppContext = (GenericWebApplicationContext) fooApp.run();

      FooService fooServiceMock = Mockito.mock(FooService.class);
      Mockito.doReturn("mockified foo !").when(fooServiceMock).yield();
      fooAppContext.removeBeanDefinition("fooService");
      fooAppContext.registerBean("fooService", FooService.class, () -> fooServiceMock);
    }

    RestTemplate restTemplate = new RestTemplate();
    String barResourceUrl = "http://localhost:8181/bar";
    ResponseEntity<String> barResponse = restTemplate.getForEntity(barResourceUrl, String.class);

    String fooResourceUrl = "http://localhost:8282/foo";
    ResponseEntity<String> fooResponse = restTemplate.getForEntity(fooResourceUrl, String.class);

    System.out.println(barResponse.getBody());
    System.out.println(fooResponse.getBody());
  }

}

Launching the test produces :

mockified bar !
mockified foo !

By the way I doubt your projects will be as simple as my example and I suspect you will run into issues related to important things I highlighted earlier.

YOU need to configure @ContextConfiguration to point both apps

Given two packages com.foo.module1, and com.foo.module2 you have to create a Configuration class per package. For example for module1:

@SpringBootApplication public class Config1 {}

If you want to run the application by using only Spring beans of a single package you can do that by using the SpringApplicationBuilder. A working snippet:

   new SpringApplicationBuilder(com.foo.module1.Config1.class)
     .showBanner(false)
     .run()

That would boot up Spring with Config1, which only searches (@ComponentScan is included in @SpringBootApplication) in its package for beans.

If you have want to run the complete application, eg all two modules at once, you'de have to create a configuration class in the upper packages com.foo.

In the case that was mentioned below, where running the two modules within a single application might probably interfere with each other in an undesired way due to libraries like the spring-boot-starters, I can only think of two possibilities:

  1. Using OSGi: Which might not solve the issue completely and might turn out be quite a complex setup or
  2. Splitting the application into two applications and creating interfaces. Spring Boot is also a good choice for a Microservice architecture.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM