26

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");
    }

}
sinned
  • 503
  • 1
  • 7
  • 25
Kristo Aun
  • 526
  • 7
  • 18
  • As of Spring Boot Test 1.5.0 you can [supposedly](https://github.com/spring-projects/spring-boot/issues/8000) use `@ContextConfiguration` with `@SpringBootTest`...have you tried using child contexts? – Peter Davis Apr 26 '17 at 03:20
  • Have you found an answer to this question ? I am in the same situation. My application is split in three modules which have their own application context and I export common beans to the parent context and that's how sibling contexts can share beans. The problem is how to test. I came to know about ContextHierarchy annotation but it creates parent child hierarchy and no sibling contexts can be created – Abdul Fatah Mar 22 '20 at 09:02
  • Here are some good suggestions I think https://stackoverflow.com/a/50040683/1207155 – RobbingDaHood Jan 24 '22 at 18:32

3 Answers3

2

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.

cerdoc
  • 473
  • 2
  • 8
0

YOU need to configure @ContextConfiguration to point both apps

Procrastinator
  • 2,526
  • 30
  • 27
  • 36
vargashj
  • 33
  • 6
-1

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, e.g. 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.
rainerhahnekamp
  • 1,087
  • 12
  • 27
  • 1
    If my spring-boot applications are in different modules, how do I get them in one Test? I would have to use a third "integration test module" that knows both applications But then I transetively get all the starters of those two applications in one classpath which might have side effects (application "A" adopts behavior of application "B" just becuase now a spring-data starter is on the path). How do you deal with that? – Jan Galinski Jun 17 '16 at 08:16
  • I've adapted my answer to your questions. – rainerhahnekamp Jun 17 '16 at 10:45
  • 1
    Yes I see. But that doesn't help me. Of course I already split up my "application" to "microservices". Thats why I want to integration test both running applications, and that what brought up the question "how?" in the first place. – Jan Galinski Jun 18 '16 at 09:14
  • 5
    If you want to test the interplay of two applications or a system that depends of multiple applications you are not on unit test-level any more. You are dealing here with a system/integration test, which means your tests access the system like any enduser or other external system via the public API. If you are running on a modern microservices architecture, your interfaces will propably run via HTTP and your testing framework will have to provide that. Checkout Selenium, Cucumber or alternatives for that. – rainerhahnekamp Jun 19 '16 at 16:37
  • 2
    This answer should be deleted as it clearly doesn't answer the question as stated which is how to test. – opticyclic Oct 09 '19 at 16:58