Ich teste eine Spring Boot-Anwendung. Ich habe mehrere Testklassen, von denen jeder einen anderen Satz von verspotteten oder anderweitig angepassten Beans benötigt.
Hier ist eine Skizze des Setups:
src/main/Java:
package com.example.myapp;
@SpringBootApplication
@ComponentScan(
basePackageClasses = {
MyApplication.class,
ImportantConfigurationFromSomeLibrary.class,
ImportantConfigurationFromAnotherLibrary.class})
@EnableFeignClients
@EnableHystrix
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
package com.example.myapp.feature1;
@Component
public class Component1 {
@Autowired
ServiceClient serviceClient;
@Autowired
SpringDataJpaRepository dbRepository;
@Autowired
ThingFromSomeLibrary importantThingIDontWantToExplicitlyConstructInTests;
// methods I want to test...
}
src/test/Java:
package com.example.myapp;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithFakeCommunication {
@Autowired
Component1 component1; // <-- the thing we're testing. wants the above mock implementations of beans wired into it.
@Autowired
ServiceClient mockedServiceClient;
@Configuration
static class ContextConfiguration {
@Bean
@Primary
public ServiceClient mockedServiceClient() {
return mock(ServiceClient.class);
}
}
@Before
public void setup() {
reset(mockedServiceClient);
}
@Test
public void shouldBehaveACertainWay() {
// customize mock, call component methods, assert results...
}
}
package com.example.myapp;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithRealCommunication {
@Autowired
Component1 component1; // <-- the thing we're testing. wants the real implementations in this test.
@Autowired
ServiceClient mockedServiceClient;
@Before
public void setup() {
reset(mockedServiceClient);
}
@Test
public void shouldBehaveACertainWay() {
// call component methods, assert results...
}
}
Das Problem mit dem obigen Setup ist, dass der in MyApplication konfigurierte Komponentenscan Component1TestWithFakeCommunication.ContextConfiguration aufruft, sodass ich einen Mock-ServiceClient auch in Component1TestWithRealCommunication bekomme, wo ich die echte ServiceClient-Implementierung haben möchte.
Obwohl ich @Autowired-Konstruktoren verwenden und die Komponenten in beiden Tests selbst aufbauen konnte, gibt es eine ausreichende Menge an komplizierten Setups, die ich Spring TestContext lieber für mich eingerichtet hätte (zum Beispiel Spring Data JPA-Repositorys, Komponenten aus Bibliotheken) außerhalb der App, die Beans aus dem Spring-Kontext abrufen usw.). Wenn Sie eine Spring-Konfiguration im Test verschachteln, die bestimmte Bean-Definitionen im Spring-Kontext lokal überschreiben kann, sollte dies ein sauberer Weg sein. Der einzige Nachteil ist, dass sich diese geschachtelten Konfigurationen auf alle Spring TestContext-Tests auswirken, die ihre Konfiguration auf MyApplication basieren (welche Komponente das App-Paket scannt).
Wie ändere ich mein Setup so, dass ich immer noch einen "meist echten" Spring-Kontext für meine Tests bekomme, mit nur ein paar lokal überschriebenen Beans in jeder Testklasse?
Das Folgende soll Ihnen helfen, Ihr Ziel zu erreichen, indem Sie ein neues fake-communication
profile einführen, das nur für die aktuelle Testklasse gilt.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles({"test", "fake-communication"})
public class Component1TestWithFakeCommunication {
// @Autowired ...
@Profile("fake-communication")
@Configuration
static class ContextConfiguration {
@Bean
@Primary
public ServiceClient mockedServiceClient() {
return mock(ServiceClient.class);
}
}
}
Wenn Sie einen @SpringBootTest
haben, können Sie einfach den zu kommentierenden Dienst mit @MockBean
versehen. So einfach ist das.
Ich würde ein paar Dinge tun:
@ComponentScan
ing zu vermeiden.Component1TestWithFakeCommunication
@SpringApplicationConfiguration(classes = MyApplication.class)
in @SpringApplicationConfiguration(classes = {MyApplication.class, Component1TestWithFakeCommunication.ContextConfiguration.class})
.Das sollte Spring ausreichend Informationen geben, um die Test-Beans zu verspotten, aber es sollte verhindern, dass die Laufzeit ApplicationContext
auch Ihre Test-Beans bemerkt.
Sie können zusätzliche explizite Profile verwenden, um zu verhindern, dass solche Testkonfigurationen erfasst werden (wie in einer anderen Antwort vorgeschlagen). Ich habe es auch getan und sogar einige Bibliotheksunterstützung dafür geschaffen.
Spring-Boot ist jedoch clever und verfügt über einen integrierten "Typ-Filter", um dieses Problem automatisch zu beheben. Damit dies funktioniert, müssen Sie Ihre @ComponentScan
-Anmerkung entfernen, wodurch Ihre Testkonfigurationen gefunden werden und der @SpringBootApplication
die Arbeit erledigen lässt. Entfernen Sie in Ihrem Beispiel einfach das:
@SpringBootApplication
@ComponentScan(
basePackageClasses = {
MyApplication.class,
ImportantConfigurationFromSomeLibrary.class,
ImportantConfigurationFromAnotherLibrary.class})
und ersetze es mit:
@SpringBootApplication(scanBasePackageClasses= {
MyApplication.class,
ImportantConfigurationFromSomeLibrary.class,
ImportantConfigurationFromAnotherLibrary.class})
Möglicherweise müssen Sie Ihren Test auch als @SpringBootTest
kommentieren. Dies sollte das automatische Scannen von Konfigurationen (und Komponenten) der inneren Klasse mit Ausnahme derjenigen, die sich im current test befinden, vermeiden.