2020. 3. 1. 01:34ㆍWeb/Spring
오늘 다루어볼 내용은 테스트 코드에서 DB관련된 테스트를 작성하는 방법이다. 사실 지금까지 여러가지 유닛테스트를 짜면서 로직에 대한 검증을 하긴 했지만, 데이터베이스와 관련된 테스트 작성은 조금 꺼려하기는 했다. 이유는 여러가지이지만, 귀찮은 설정들이 필요하고 사실 외부 환경에 따라 테스트가 실패할 가능성도 있기 때문이다. 하지만 계속 미룰수는 없는 법.. 오늘은 간단하게 데이터베이스관련 된 테스트를 Junit으로 만들어볼 것이다.
환경
- springboot
- mongodb
- junit
다루어볼 예제는 정말 간단한 User 객체에 대한 생성,수정,조회 등을 테스트해볼 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
buildscript {
ext {
flapdoodleVersion = "2.2.0"
}
}
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '13'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'io.projectreactor:reactor-test'
testImplementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:${flapdoodleVersion}"
-> 임베디드 몽고 의존
}
test {
useJUnitPlatform()
}
|
cs |
build.gradle 내용이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class User {
@JsonIgnore
private ObjectId id;
@NotNull
private String username;
@NotNull
private Integer age;
@NotNull
private Address address;
private String hobby;
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public static class Address {
@NotNull
private String si;
@NotNull
private String gu;
@NotNull
private String dong;
}
}
|
cs |
간단한 유저 도메인 객체이다.
1
2
3
4
5
|
public interface UserRepository {
Mono<User> findUserById(final ObjectId id);
Mono<User> updateUser(final User user);
Mono<User> createUser(final User user);
}
|
cs |
해당 인터페이스는 DB Access 구현 방식에 구애 받지 않고 core한 영역에서 사용하기 위한 User Data Access 인터페이스이다. 나머지 코드를 보면 왜 구현 코드에 구애 받지 않는지 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@Document("User")
public class UserData {
@Id
private ObjectId id;
private String name;
private Integer age;
private AddressData userAddress;
private String hobby;
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public static class AddressData {
private String si;
private String gu;
private String dong;
public User.Address from() {
return User.Address.of(
si,
gu,
dong
);
}
public static UserData.AddressData to(User.Address address) {
return UserData.AddressData.of(
address.getSi(),
address.getGu(),
address.getDong()
);
}
}
public User from() {
return User.of(
id,
name,
age,
userAddress.from(),
hobby
);
}
public static UserData to(User user) {
return UserData.of(
user.getId(),
user.getUsername(),
user.getAge(),
AddressData.to(user.getAddress()),
user.getHobby()
);
}
}
|
cs |
실제 DB Access를 위한 엔티티 클래스이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final UserMongoRepository repository;
@Override
public Mono<User> findUserById(final ObjectId id) {
return repository.findById(id).map(UserData::from);
}
@Override
public Mono<User> updateUser(final User user) {
return repository.save(UserData.to(user)).map(UserData::from);
}
@Override
public Mono<User> createUser(final User user) {
return repository.save(UserData.to(user)).map(UserData::from);
}
}
|
cs |
실제 DB Access 구현체를 필드에 가지고 있는 UserRepository의 구현체이다. 즉, core(프레임워크에 영향을 받지 않는 비지니스 영역)한 영역에서 UserRepository를 사용할때는 구현체에 대한 자세한 정보가 필요하지 않고 DI해서 사용하면 되는 것이다.
1
|
public interface UserMongoRepository extends ReactiveMongoRepository<UserData, ObjectId> { }
|
cs |
UserRepositoryImpl가 가지고 있는 DB Access Object 구현체이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Configuration
@EnableReactiveMongoRepositories(basePackages = {"com.example.test.data.db.repositories"}, reactiveMongoTemplateRef = "userMongoTemplate")
public class MongoConfig {
@Bean
public MongoClient mongoClient() {
return MongoClients.create("mongodb://localhost:27017");
}
@Bean
public ReactiveMongoTemplate userMongoTemplate(MongoClient mongoClient) {
return new ReactiveMongoTemplate(mongoClient, "USER_DB");
}
}
|
cs |
몽고디비 설정이다. 여기까지 실제 동작하는 코드 영역의 클래스들이다. 이러한 클래스들이 잘 동작하길 원할 것이고, 실제 테스트를 해볼 것이지만 효율적인 테스트 방법이 무엇일까? 관련 API를 만들어서 직접 호출해본다? 과연 좋은 테스트 방법인지는 모르겠다. 이러한 DB 관련 테스트 코드를 짜보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@TestConfiguration
@EnableReactiveMongoRepositories(basePackages = {
"com.example.test.data.db.repositories"
}, reactiveMongoTemplateRef = "userTestMongoTemplate")
public class TestEmbeddedMongoConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public MongodExecutable mongodExecutable() throws IOException {
MongodStarter starter = MongodStarter.getDefaultInstance();
IMongodConfig mongoDConfig = new MongodConfigBuilder()
.version(Version.Main.V3_4)
.net(new Net("127.0.0.1", 27017, false))
.build();
return starter.prepare(mongoDConfig);
}
@Bean("userTestMongoTemplate")
public ReactiveMongoTemplate mongoTemplate() throws IOException {
MongoClient mongo = MongoClients.create("mongodb://localhost:27017");
return new ReactiveMongoTemplate(mongo, "USER_TEST_DB");
}
}
|
cs |
해당 설정은 테스트 코드에서만 사용하기 위한 설정이다. 몽고디비는 임베디드 몽고를 사용할 것이다. 필자로 사실 내부적으로 정확히 어떻게 동작하는지는 자세히 보지 않았지만, 실제 특정 포트를 물고 프로세스로 뜬다. 그래서 직접 포트 번호, ip를 기입해준다. 설정들이 다양하지만 간단히 예제를 다루어보는 것임으로 간단하게 설정을 마친다.
다음은 실제 테스트 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
@ContextConfiguration(
initializers = ConfigFileApplicationContextInitializer.class,
classes = {
TestEmbeddedMongoConfig.class,
UserRepositoryImpl.class
})
@ExtendWith(SpringExtension.class)
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
private User initData;
@BeforeEach
public void setup() {
initData = userRepository.createUser(
User.of(
new ObjectId(),
"yeoseonggae",
29,
User.Address.of("seoul", "kangbuk", "mia"),
"none"
)
).block();
}
@Test
@DisplayName("유저 데이터 생성 테스트")
public void createUserTest() {
//준비
User createUser = User.of(
new ObjectId(),
"UserName",
29,
User.Address.of("seoul", "kangbuk", "mia"),
"none"
);
//실행
Mono<User> createdUser = userRepository.createUser(createUser);
//단언
StepVerifier.create(createdUser)
.assertNext(user -> {
assertEquals(user.getUsername(), "UserName");
assertEquals(user.getAge(), 29);
assertEquals(user.getHobby(), "none");
assertEquals(user.getAddress().getSi(), "seoul");
}).verifyComplete();
}
@Test
@DisplayName("유저 데이터 수정 테스트")
public void updateUserTest() {
//준비
initData.setUsername("여성게");
//실행
Mono<User> updatedUser = userRepository.updateUser(initData);
//단언
StepVerifier.create(updatedUser)
.assertNext(user -> {
assertEquals(user.getId().toHexString(), initData.getId().toHexString());
assertEquals(user.getUsername(), "여성게");
assertEquals(user.getAge(), 29);
assertEquals(user.getHobby(), "none");
assertEquals(user.getAddress().getSi(), "seoul");
}).verifyComplete();
}
@Test
@DisplayName("유저 이름으로 유저 데이터 조회 테스트")
public void findUserByNameTest() {
//준비
final ObjectId id = initData.getId();
//실행
Mono<User> findUser = userRepository.findUserById(id);
//단언
StepVerifier.create(findUser)
.assertNext(user -> {
assertEquals(user.getUsername(), "yeoseonggae");
assertEquals(user.getAge(), 29);
assertEquals(user.getHobby(), "none");
assertEquals(user.getAddress().getSi(), "seoul");
}).verifyComplete();
}
}
|
cs |
바로 위에 있는 테스트 몽고 설정을 import 한다. 그리고 우리는 UserRepository를 테스트 할것임으로 모든 빈들을 가져올 필요가 없이 테스트에 필요한 빈만 주입받아서 테스트 코드를 작성한다. 과연 결과는 어떻게 될까?(위 코드에 조금더 추가하여 실제로 데이터가 어떻게 왔다갔다하는지 콘솔로 찍었다.)
우선 테스트 성공 여부이다.
크아 깔끔하게 테스트를 통과하였다. 그렇다면 진짜 데이터가 왔다갔다 한것이 맞을까? 콘솔로 찍힌 로그를 확인해보자 !
유저 데이터 수정 테스트 로그이다.
유저 데이터 생성 테스트 로그이다.
마지막으로 조회 테스트 로그이다. 실제로 데이터가 잘 찍혀있다. 여기까지 간단히 몽고디비를 이용한 DB Access unit 테스트를 다루어보았다. 정말 간단한 테스트이지만, 이 예제를 잘 이용하면 실제 서비스 클래스 영역까지 사용되어서 정말 내가 개발한 클래스가 정상 작동하는지 확인해볼 수 있다.
혹시 코드가 필요하다면 아래 깃헙를 참고하자.