Compare commits

1 Commits

Author SHA1 Message Date
Rebwon
a5c27bda82 Using @Async annotation and WebTestClient
Changed synchronous mail transfer to asynchronous. You also switched to
the WebTest Client without using MockMvc, which is dependent on Spring
ApplicationContext. As a result, the test runs 60% faster.
2021-09-02 11:47:46 +09:00
7 changed files with 128 additions and 47 deletions

View File

@@ -45,9 +45,11 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto:5.5.2'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'io.github.javaunit:autoparams:0.2.12'
testImplementation 'com.tngtech.archunit:archunit-junit5:0.20.1'
testImplementation 'org.mockito:mockito-inline:3.9.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

View File

@@ -0,0 +1,28 @@
package com.yam.app.account.infrastructure;
import java.util.concurrent.Executor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Slf4j
@EnableAsync
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
var executor = new ThreadPoolTaskExecutor();
int processors = Runtime.getRuntime().availableProcessors();
log.info("processors count {}", processors);
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors * 2);
executor.setQueueCapacity(50);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}

View File

@@ -3,12 +3,13 @@ package com.yam.app.account.infrastructure;
import com.yam.app.account.domain.RegisterAccountEvent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
@Component
final class MailManager {
class MailManager {
private final MailDispatcher mailDispatcher;
private final TemplateEngine templateEngine;
@@ -21,6 +22,7 @@ final class MailManager {
this.host = host;
}
@Async
@EventListener
public void handle(RegisterAccountEvent event) {
var newAccount = event.getAccount();

View File

@@ -5,10 +5,8 @@ import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.sli
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Disabled;
@AnalyzeClasses(packagesOf = YouAndMeApplication.class)
@Disabled
final class CircularDependencyTests {
@ArchTest

View File

@@ -0,0 +1,27 @@
package com.yam.app;
import static org.mockito.Mockito.mockStatic;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.boot.SpringApplication;
final class YouAndMeApplicationTests {
@Test
@DisplayName("SpringApplication.run() 실행을 Mocking합니다.")
void mainShouldStartMyApplication() {
try (MockedStatic<SpringApplication> mocked = mockStatic(SpringApplication.class)) {
mocked.when(
() -> SpringApplication.run(YouAndMeApplication.class, "foo"))
.thenReturn(null);
YouAndMeApplication.main(new String[]{"foo"});
mocked.verify(
() -> SpringApplication.run(YouAndMeApplication.class, "foo"));
}
}
}

View File

@@ -1,12 +1,8 @@
package com.yam.app.account.integration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.account.presentation.RegisterAccountRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,7 +10,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
@SpringBootTest
@AutoConfigureMockMvc
@@ -28,6 +26,15 @@ final class AccountIntegrationTests {
@Autowired
private ObjectMapper objectMapper;
private WebTestClient webTestClient;
@BeforeEach
void setUp() {
this.webTestClient = MockMvcWebTestClient
.bindTo(mockMvc)
.build();
}
@Test
@DisplayName("새로운 계정을 등록하는 회원가입 시나리오")
void register_success() throws Exception {
@@ -38,18 +45,20 @@ final class AccountIntegrationTests {
request.setPassword("password!");
// Act
final var actions = mockMvc.perform(post("/api/accounts")
var spec = webTestClient
.post()
.uri("/api/accounts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
.bodyValue(objectMapper.writeValueAsString(request))
.exchange();
// Assert
actions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.email").isString())
.andExpect(jsonPath("$.nickname").isString());
spec
.expectStatus().isOk()
.expectBody()
.jsonPath("$.id").isNumber()
.jsonPath("$.email").isNotEmpty()
.jsonPath("$.nickname").isNotEmpty();
}
}

View File

@@ -1,14 +1,11 @@
package com.yam.app.account.presentation;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.account.application.AccountFacade;
import org.javaunit.autoparams.AutoSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -16,8 +13,11 @@ import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
@DisplayName("회원가입 등록 HTTP API")
@WebMvcTest(RegisterAccountApi.class)
@@ -30,6 +30,15 @@ class RegisterAccountApiTests {
@MockBean
private AccountFacade accountFacade;
private WebTestClient webTestClient;
@BeforeEach
void setUp() {
this.webTestClient = MockMvcWebTestClient
.bindTo(mockMvc)
.build();
}
@Test
@DisplayName("회원가입에 적절한 파라미터가 입력되고 회원가입이 성공한다.")
void register_success() throws Exception {
@@ -43,19 +52,21 @@ class RegisterAccountApiTests {
when(accountFacade.register(request)).thenReturn(
new AccountResponse(1L, "msolo021015@gmail.com", "rebwon"));
final var actions = mockMvc.perform(post("/api/accounts")
var spec = webTestClient
.post()
.uri("/api/accounts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
.bodyValue(objectMapper.writeValueAsString(request))
.exchange();
// Assert
actions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.email").isString())
.andExpect(jsonPath("$.nickname").isString());
spec
.expectStatus().isOk()
.expectBody()
.jsonPath("$.id").isNumber()
.jsonPath("$.email").isNotEmpty()
.jsonPath("$.nickname").isNotEmpty();
}
@Test
@@ -68,13 +79,15 @@ class RegisterAccountApiTests {
request.setPassword("password!");
// Act
final var actions = mockMvc.perform(post("/api/accounts")
.content(objectMapper.writeValueAsString(request))
);
var spec = webTestClient
.post()
.uri("/api/accounts")
.bodyValue(objectMapper.writeValueAsString(request))
.exchange();
// Assert
actions
.andExpect(status().isUnsupportedMediaType());
spec
.expectStatus().isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
@ParameterizedTest
@@ -88,16 +101,17 @@ class RegisterAccountApiTests {
request.setPassword(arg);
// Act
final var actions = mockMvc.perform(post("/api/accounts")
var spec = webTestClient
.post()
.uri("/api/accounts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
.bodyValue(objectMapper.writeValueAsString(request))
.exchange();
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
spec
.expectStatus().isBadRequest();
}
@ParameterizedTest
@@ -111,15 +125,16 @@ class RegisterAccountApiTests {
request.setPassword(arg);
// Act
final var actions = mockMvc.perform(post("/api/accounts")
var spec = webTestClient
.post()
.uri("/api/accounts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
);
.bodyValue(objectMapper.writeValueAsString(request))
.exchange();
// Assert
actions
.andDo(print())
.andExpect(status().isBadRequest());
spec
.expectStatus().isBadRequest();
}
}