In diesem Artikel werden die Überlegungen zum Entwerfen einer Unit-Test-Umgebung zusammengefasst, die in einer Webanwendung, die Spring Boot verwendet, einfach zu implementieren und zu warten ist. Da es sich um einen Artikel aus dem Design handelt, geht es nicht darum, wie Testcode konkret geschrieben wird.
Umgebung
Referenz
In dieser Demoanwendung werden die folgenden drei Unterpakete für jede Ebene erstellt. Das Aufteilen der Ebenen in Unterpakete erleichtert das Erstellen einer Testumgebung für jede Ebene.
src.main.java
|
+--- com.example.demo //★package root
| |
| +--- Application.java //★Main Application Class
| |
| +--- domain //★ Domain-Schicht
| | |
| | +--- DatasourceConfig.java //★ Datenquellenkonfiguration
| | |
| | +--- entity //☆ Platzieren Sie die JPA-Entitätsklasse
| | |
| | +--- repository //☆ Platzieren Sie die JPA-Repository-Schnittstelle
| | | (spring-data-jpa)
| | +--- service //☆ Platzieren Sie die Geschäftslogikklasse
| | |
| | +--- impl
| |
| +--- external //★ Externe Schicht
| | |
| | +--- service //☆ Platzieren Sie die Geschäftslogikklasse
| | |
| | +--- impl
| |
| +--- web //★ Webebene
| |
| +--- WebMvcConfig.java //★ WebMvc-Konfiguration
| +--- JacksonConfig.java //★ Jackson-Konfiguration
| |
| +--- advice
| | |
| | +--- CustomControllerAdvice.java
| |
| +--- interceptor
| | |
| | +--- CustomHandlerInterceptor.java
| |
| +--- controller //☆ Controller-Klasse platzieren
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- DomainTestApplication.java //★ Hauptanwendungsklasse für den Domain-Layer-Test
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- ExternalTestApplication.java //★ Hauptanwendungsklasse für externe Schichttests
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebTestApplication.java //★ Web Tier Test Hauptanwendungsklasse
| |
| +--- controller
|
src.test.resources
|
+--- application.yml
In Spring Boot wird empfohlen, die Hauptanwendungsklasse mit der Spring Boot-Anwendungsanmerkung im Paketstamm (oder einem Paket, das höher als andere Klassen ist) zu platzieren. Wenn Sie diese Klasse über andere Klassen stellen, werden die untergeordneten Komponenten (mit Komponenten- und Dienstanmerkungen) und Konfigurationsklassen automatisch gescannt.
14.2 Locating the Main Application Class We generally recommend that you locate your main application class in a root package above other classes
Diese Klasse ist allgemeiner (allgemeiner) Inhalt, daher gibt es nichts Besonderes zu erwähnen.
Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Die auf die Domänenschicht bezogene Konfigurationsklasse wird direkt unter dem Domänenpaket platziert. Dieses Beispiel ist eine Klasse, die die Datenquelle konfiguriert. Ich habe es in diesem Code nicht konfiguriert, aber ich gehe davon aus, dass es in dieser Klasse ist, wenn Sie die Datenquelle oder den Transaktionsmanager anpassen müssen.
DatasourceConfig
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EntityScan
public class DatasourceConfig {
// nothing
}
Obwohl im obigen Code weggelassen, können Sie auch die Pakete angeben, die in basePackages gescannt werden sollen, wie unten gezeigt.
@EnableJpaRepositories(basePackages = {"com.example.demo.domain.repository"})
@EntityScan(basePackages = {"com.example.demo.domain.entity"})
Die auf die Webebene bezogene Konfigurationsklasse wird direkt unter dem Webpaket platziert. Dieses Beispiel ist eine Klasse, die WebMvc und anpasst
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomHandlerInterceptor())
.addPathPatterns("/memo/**");
}
}
Dies ist eine Klasse zum Anpassen von Jackson. Der folgende Code passt den ObjectMapper an, aber Sie können dasselbe mit der Konfigurationsdatei tun.
JacksonConfig
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL)
.indentOutput(true)
.failOnUnknownProperties(false)
.failOnEmptyBeans(false)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);
return builder;
}
}
Für jede Schicht werden Testcode-Einstiegspunktklassen vorbereitet. Durch die Vorbereitung für jede Ebene können Sie nur die erforderlichen Abhängigkeiten erfassen.
Das Paketstammverzeichnis für das Testen der Domänenebene lautet com.example.demo.domain, und die DomainTestApplication-Klasse ist der Einstiegspunkt für das Testen.
src.main.java
|
+--- com.example.demo
| |
| +--- domain
| |
| +--- DatasourceConfig.java //★ Datenquellenkonfiguration
| |
| +--- entity
| |
| +--- repository
| |
| +--- service
| |
| +--- impl
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| |
| +--- DomainTestApplication.java //★ Hauptanwendungsklasse für den Domain-Layer-Test
| |
| +--- entity
| |
| +--- repository
| |
| +--- service
| |
| +--- impl
|
src.test.resources
|
+--- application.yml
DomainTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DomainTestApplication {
public static void main(String[] args) {
SpringApplication.run(DomainTestApplication.class, args);
}
}
[DataJpaTest] für Unit-Test des Repositorys (https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.html) Verwenden Sie Anmerkungen. Wenn Sie die Annotation DataJpaTest hinzufügen, wird die eingebettete Datenbank unabhängig von den Datenquelleneinstellungen verwendet. (H2 wird in dieser Demo-Anwendung verwendet) Anstelle von EntityManager auch TestEntityManager ) Ist verfügbar.
@RunWith(SpringRunner.class)
@DataJpaTest
public class MemoRepositoryTests {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private MemoRepository memoRepository;
//Testcode
}
Wenn Sie den Integrationstest mit der in der Konfigurationsdatei definierten Datenquelle anstelle der eingebetteten Datenbank durchführen möchten, wählen Sie AutoConfigureTestDatabase. org / springframework / boot / test / autoconfigure / jdbc / AutoConfigureTestDatabase.html) Sie können die Einstellung mit der Anmerkung ändern.
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemoRepositoryIntegrationTests {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private MemoRepository memoRepository;
//Testcode
}
Wenn Sie eine SQL-Datei oder eine SQL-Anweisung zur Eingabe von Testdaten verwenden möchten, wählen Sie SQL /context/jdbc/Sql.html) Anmerkungen sind verfügbar. Anmerkungen können an Klassen und Methoden angehängt werden. Wenn sie jedoch wie in JavaDoc beschrieben an beide angehängt werden, haben die Methodeneinstellungen Vorrang.
Method-level declarations override class-level declarations.
@Sql(statements = {
"INSERT INTO memo (id, title, description, done, updated) VALUES (11, 'test title 1', 'test description', false, CURRENT_TIMESTAMP)",
"INSERT INTO memo (id, title, description, done, updated) VALUES (12, 'test title 2', 'test description', true, CURRENT_TIMESTAMP)",
"INSERT INTO memo (id, title, description, done, updated) VALUES (13, 'test title 3', 'test description', false, CURRENT_TIMESTAMP)",
})
AutoConfigureTestEntityManager Verwenden Sie die Anmerkung In diesem Fall kann TestEntityManager ohne die Annotation DataJpaTest verwendet werden.
Spring Runner ist für Unit-Tests von Diensten nicht erforderlich, da es nicht vom Spring Framework abhängt. Die Komponenten, von denen das Testziel abhängt, werden in Mockito verspottet (oder ausspioniert).
public class MemoServiceImplTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Mock
private MemoRepository memoRepository;
@InjectMocks
private MemoServiceImpl sut;
//Testcode
}
Verwenden Sie die Annotation SpringBootTest zum Testen der Service-Integration Machen. Da die Webserverfunktion nicht erforderlich ist, wird für webEnvironment auch WebEnvironment.NONE angegeben. Bei der Einstellung NONE wird der integrierte Webserver nicht gestartet. Die Datenquelle der Konfigurationsdatei wird für den Zugriff auf die Datenbank verwendet. Sie können die oben erwähnte SQL-Annotation oder den EntityManager mit dem folgenden Code verwenden, um Testdaten einzugeben. Andere in [80. Datenbankinitialisierung] beschriebene Methoden (https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/reference/htmlsingle/#howto-database-initialization) Es gibt.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
public class MemoServiceImplIntegrationTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoServiceImpl sut;
//Testcode
}
Das Paketstammverzeichnis für Tests auf externen Ebenen lautet com.example.demo.external, und die ExternalTestApplication-Klasse ist der Einstiegspunkt für das Testen.
src.main.java
|
+--- com.example.demo
| |
| +--- external
| |
| +--- service
| |
| +--- impl
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- external
| |
| +--- ExternalTestApplication.java //★ Hauptanwendungsklasse für externe Schichttests
| |
| +--- service
| |
| +--- impl
|
src.test.resources
|
+--- application.yml
Schließen Sie DataSourceAutoConfiguration von AutoConfiguration aus, da die externe Schicht datenbankunabhängig ist.
ExternalTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
public class ExternalTestApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalTestApplication.class, args);
}
}
Da es sich um eine Testklasse handelt, die dem Test des Dienstes der Domänenschicht ähnlich ist, wird sie weggelassen.
Das Paketstammverzeichnis für das Testen der Webebene lautet com.example.demo.web, und die WebTestApplication-Klasse ist der Einstiegspunkt für das Testen.
src.main.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- DatasourceConfig.java //★ Datenquellenkonfiguration
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebMvcConfig.java //★ WebMvc-Konfiguration
| +--- JacksonConfig.java //★ Jackson-Konfiguration
| |
| +--- advice
| | |
| | +--- MyControllerAdvice.java
| |
| +--- interceptor
| | |
| | +--- MyHandlerInterceptor.java
| |
| +--- controller
|
src.java.resources
|
+--- application.yml
src.test.java
|
+--- com.example.demo
| |
| +--- domain
| | |
| | +--- entity
| | |
| | +--- repository
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- external
| | |
| | +--- service
| | |
| | +--- impl
| |
| +--- web
| |
| +--- WebTestApplication.java //★ Web Tier Test Hauptanwendungsklasse
| |
| +--- controller
|
src.test.resources
|
+--- application.yml
Da die Webschicht von der Domänenschicht und der externen Schicht abhängt, wird das Zielpaket in scanBasePackages angegeben.
WebTestApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {
"com.example.demo.domain.service",
"com.example.demo.external.service",
"com.example.demo.web"
})
public class WebTestApplication {
public static void main(String[] args) {
SpringApplication.run(WebTestApplication.class, args);
}
}
Fügen Sie in Jsons Komponententest die Anmerkung JsonTest hinzu. Ich werde es benutzen. Dies ist ein Komponententest, um festzustellen, ob das Ergebnis der Konvertierung einer Entität in json wie erwartet serialisiert wird. Testen Sie auf Endlosschleifen, wenn Sie die Serialisierung mit Jacksons JsonProperty- oder JsonIgnore-Annotationen anpassen oder wenn zwischen Entitäten gegenseitige Verweise bestehen, insbesondere mit JPA-bezogenen Annotationen (OneToMany, ManyToOne usw.).
@RunWith(SpringRunner.class)
@JsonTest
public class MemoToJsonTests {
@Autowired
private JacksonTester<Memo> json;
//Testcode
}
WebMvcTest Verwenden Sie Anmerkungen. WebMvcTest kann MockMvc und WebClient verwenden (für die Abhängigkeit ist HtmlUnit erforderlich).
Das Verspotten der Komponenten, von denen der zu testende Controller abhängt, ist Spring Boots MockBean /mock/mockito/MockBean.html) (Spy Bean [SpyBean](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/mock /mockito/SpyBean.html)) Verwenden Sie Anmerkungen. Von MockBean verspottete Objekte werden dem Anwendungskontext hinzugefügt und in das Testziel (in diesem Beispiel MemoController) eingefügt.
@RunWith(SpringRunner.class)
@WebMvcTest(MemoController.class)
public class MemoControllerTests {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MemoService memoService;
//Testcode
}
AutoConfigureMockMvc Verwenden Sie die Anmerkung In diesem Fall können Sie MockMvc ohne die Annotation WebMvcTest verwenden.
In Testklassen, die die SpringBootTest-Annotation anstelle von RestTemplate verwenden, TestRestTemplate /client/TestRestTemplate.html) ist verfügbar. Importieren Sie die DatasourceConfig-Klasse, da für den Controller-Integrationstest Zugriff auf die Datenbank erforderlich ist.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(value = {DatasourceConfig.class})
public class MemoControllerIntegrationTests {
@Autowired
private TestRestTemplate testRestTemplate;
//Testcode
}
Durch Definieren einer Einstellungsklasse wie dem folgenden Code können Sie die Datenquelle in jedem Integrationstest durch die eingebettete Datenbank ersetzen.
@TestConfiguration
public class WebTestConfig {
@Bean
public DataSource datasource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScripts("classpath:scripts/init.sql")
.build();
}
}
TestConfiguration Die Konfigurationsklasse, die Anmerkungen verwendet, ist SpringBootConfiguration. Es unterliegt keiner automatischen Erkennung von Anmerkungen und muss manuell importiert werden.
@Import(value = {DatasourceConfig.class, WebTestConfig.class})
Übrigens ist es beim Erstellen eines Schemas oder beim Eingeben von Testdaten schwierig zu verwenden, da es in SQL geschrieben sein muss, das auch in einer eingebetteten Datenbank verwendet werden kann.
Wenn Sie die Debug-Eigenschaft in der Einstellungsdatei festlegen, wird das Debug-Protokoll ausgegeben. (Gleiches gilt, wenn Sie in den Systemeigenschaften -Ddebug
angeben.)
debug: true
Das Ergebnis der automatischen Konfiguration "CONDITIONS EVALUATION REPORT" wird in das Debug-Protokoll ausgegeben, sodass Sie den Konfigurationsstatus überprüfen können.
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice', 'org.aspectj.weaver.AnnotatedElement'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
//Kürzung
Negative matches:
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)
//Kürzung
Exclusions:
-----------
None
Unconditional classes:
----------------------
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
//Kürzung
Beim Komponententest mit Mockito 1.x sah ich manchmal Testcode, der das zu testende private Feld durch eine Klasse namens Whitebox ersetzte, aber seit Mockito 2.1 ist diese Klasse nicht mehr verfügbar.
Whitebox.setInternalState(sut, "someService", mockSomeService);
In Spring (Boot) kann die Annotation Value den Einstellungswert in das private Feld einfügen, daher habe ich gesehen, dass er auch in solchen Fällen verwendet wird.
@Value("${app.someValue}")
private String someValue;
Der Grund, warum das Entwicklungsteam von Mockito Whitebox entfernt hat, liegt in den folgenden Ausgaben. In einer groben Übersetzung heißt es jedoch: "Die einfache Verwendung von Whitebox hat die Massenproduktion von Testcode mit schlechter Qualität gesteigert. Es scheint, dass es von ist.
Im Frühjahr (Boot) anstelle von Whitebox ReflectionTestUtils Sie können Klassen verwenden, aber wenn die reflexionsbasierte Ersetzung schlecht ist, zögern Sie, diese Methode ebenfalls zu verwenden.
Eine andere Alternative besteht darin, die Sichtbarkeit des Feldes von privat auf privat zu ändern. Da sich die Testklasse normalerweise im selben Paket befindet wie die zu testende Klasse, können die Felder direkt aus dem Testcode neu geschrieben werden. Es ist nicht ungewöhnlich, die Sichtbarkeit zu erweitern, um das Testen zu vereinfachen, z. B. VisibleForTesting für Google Guava. Es gibt eine Anmerkung (/google/common/annotations/VisibleForTesting.html). Da es sich bei dieser Anmerkung um eine Markierungsanmerkung handelt, wird der sichtbare Bereich während des Tests nicht automatisch erweitert.
Ich bin nicht mit testgetriebenen Entwicklungs- und Unit-Test-Techniken vertraut, daher kann ich nicht sagen, dass die Verwendung von Reflexion "schlecht" und die Lockerung der Sichtbarkeit "besser als" ist.
Recommended Posts