如何在一个测试中测试多个Spring Boot应用程序?

26

我有一个包含两个Spring Boot应用程序的多模块Maven项目:

父级

  • fooApp
  • barApp
  • test

如何设置一个测试,使您可以在同一进程中加载单独的Spring Boot应用程序,并具有其自己的配置上下文。

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

一个集成测试的示例

public class IntegrationTest extends AbstractIntegrationTest {

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

}

从Spring Boot Test 1.5.0开始,你可以据说@SpringBootTest中使用@ContextConfiguration...你尝试过使用子上下文吗? - Peter Davis
你找到这个问题的答案了吗?我也处于同样的情况。我的应用程序分为三个模块,每个模块都有自己的应用程序上下文,并将公共bean导出到父上下文中,这就是兄弟上下文可以共享bean的方式。问题在于如何进行测试。我了解到ContextHierarchy注释,但它创建了父子层次结构,无法创建兄弟上下文。 - Abdul Fatah
这里有一些我认为不错的建议 https://dev59.com/CKrka4cB1Zd3GeqPlOxm#50040683 - RobbingDaHood
3个回答

2

我同意@rainerhahnekamp的观点,您要实现的更像是系统/集成测试。

不过如果您还是想以这种方式进行测试,我认为这仍然是可行的。

首先,有一个重要的事情需要知道
test项目中导入fooAppbarApp项目会使两个项目的配置文件都能够被类加载器访问,并且会产生难以预测的结果。例如:只有两个application.properties文件中的一个会被加载。因此,您需要使用2个不同的配置文件设置来加载2个分离的配置文件。
由于项目文件重叠的相同原因,由一个应用程序在其他应用程序可见的包中定义的bean将在两个应用程序上下文中加载。

为了测试这个概念,我在每个项目中创建了一个服务和一个REST控制器,每个控制器都有一个“profiled”属性文件:

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

}

应用程序-foo.properties :

server.port=8282

测试

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

}

启动测试会产生:

mockified bar !
mockified foo !

顺便说一下,我怀疑你的项目不会像我的例子那样简单,我认为你会遇到与我之前强调的重要事项相关的问题。


0

你需要配置@ContextConfiguration来指向两个应用程序


-1

假设有两个包 com.foo.module1 和 com.foo.module2,您需要为每个包创建一个 Configuration 类。 例如,对于 module1:

@SpringBootApplication public class Config1 {}

如果您想仅使用单个包的Spring bean运行应用程序,可以使用SpringApplicationBuilder来实现。以下是一个可工作的代码片段:
   new SpringApplicationBuilder(com.foo.module1.Config1.class)
     .showBanner(false)
     .run()

这将使用Config1启动Spring,它仅在其包中搜索bean(@ComponentScan包含在@SpringBootApplication中)。

如果您想同时运行完整的应用程序,例如两个模块,您需要在上层包com.foo中创建一个配置类。

在下面提到的情况中,如果在单个应用程序中运行两个模块可能会由于诸如spring-boot-starters之类的库而产生意外干扰,我只能想到两种可能性:

  1. 使用OSGi:这可能无法完全解决问题,并且可能会变得相当复杂,或
  2. 将应用程序拆分为两个应用程序并创建接口。 Spring Boot也是微服务架构的不错选择。

1
如果我的Spring Boot应用程序在不同的模块中,我该如何将它们放在一个测试中?我需要使用第三个“集成测试模块”,该模块了解这两个应用程序。但是,这样会导致这两个应用程序的所有启动器都传递到一个类路径中,这可能会产生副作用(例如,应用程序“A”采用应用程序“B”的行为,只是因为现在在路径上有一个Spring Data启动器)。你是如何处理这种情况的? - Jan Galinski
我已根据您的问题进行了回答。 - rainerhahnekamp
1
是的,我明白。但这并没有帮助到我。当然,我已经将我的“应用程序”拆分为“微服务”。这就是为什么我想要集成测试两个正在运行的应用程序,并且这也引出了“如何?”的问题。 - Jan Galinski
5
如果你想测试两个应用程序之间的相互作用或依赖多个应用程序的系统,则不再处于单元测试级别。这里涉及到系统/集成测试,这意味着你的测试通过公共API像任何终端用户或其他外部系统访问系统。如果你在使用现代微服务架构,则你的接口可能通过HTTP运行,并且你的测试框架需要支持该功能。可以尝试使用Selenium、Cucumber或其他替代工具进行测试。 - rainerhahnekamp
2
这个答案应该被删除,因为它显然没有回答问题,而问题是如何进行测试。 - opticyclic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接