wake-up-neo.com

Wie verwende ich @ComponentScan zusammen mit testspezifischen ContextConfigurations in SpringJunit4TestRunner?

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?

14
Jonathan Fuerth

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);
        }
    }
}
9
Sam Brannen

Wenn Sie einen @SpringBootTest haben, können Sie einfach den zu kommentierenden Dienst mit @MockBean versehen. So einfach ist das.

3
Jorge Viana

Ich würde ein paar Dinge tun:

  1. Verschieben Sie Ihre Testklassen in ein anderes Paket, um @ComponentScaning zu vermeiden.
  2. Ändern Sie in 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.

0
Josh Ghiloni

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.

0
Marwin