서론

기존에 jar파일을 추가해서 Junit4로 테스트하던 프로젝트를 마찬가지로 jar파일을 Junit5로 버전 업해서 사용하고 있었는데
‘굳이 이럴 필요없이 maven 프로젝트로 변경하면 되지 않나?’라는 생각에 maven 프로젝트로 변경 후 발생했던 에러를 해결한 방법을 작성해둔다…

상황

위에서 적은 것 처럼 기존에 Junit jar를 다운받아서 직접 경로를 지정하는 방식으로 junit4, 5를 둘 다 사용하고 있었는데 프로젝트를 maven으로 변경 후 의존성 충돌이 발생해 아래와 같은 에러가 발생하며 테스트가 진행되지 않았다.

발생한 에러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Internal Error occurred.
org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-vintage' failed to discover tests
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:111)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:85)
at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:92)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: org.junit.platform.commons.JUnitException: Unsupported version of junit:junit: 3.8.1. Please upgrade to version 4.12 or later.
at org.junit.vintage.engine.JUnit4VersionCheck.checkSupported(JUnit4VersionCheck.java:49)
at org.junit.vintage.engine.JUnit4VersionCheck.checkSupported(JUnit4VersionCheck.java:35)
at org.junit.vintage.engine.VintageTestEngine.discover(VintageTestEngine.java:62)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:103)
... 7 more

Process finished with exit code -2

위 에러 로그처럼 ‘junit-vintage’를 아이디로 하는 테스트 엔진을 찾지 못했다는 에러가 발생해서 junit-vintage 의존성을 추가해주었는데..


그 후 삽질

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.junit.vintage/junit-vintage-engine -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>

추가 해줘도 똑같은 에러만 반복 할 뿐이었다.

그래서 공식 문서를 살펴보니..

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

라고 떡하니 써 적혀있어서 위에 3 개의 의존성을 모두 추가한 후 리빌드 후 다시 테스트를 실행해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>

하지만 결과는 마찬가지였고… .m2폴더에서 Junit 의존성을 다 삭제하고, Intellij project setting에서 Modules, Libraies에서 모두 삭제하고 이것저것 다 해봤지만 모두 실패로 돌아가서 결국 새 maven 프로젝트를 만들어서 하나씩 비교해보기로 했다.


해결

그 결과… .iml 파일에 아래 처럼 의존성들이 남아있는 것을 확인했고…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<orderEntry type="module-library" scope="TEST">
<library name="JUnit5.4">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter/5.4.2/junit-jupiter-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.4.2/junit-jupiter-api-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.0.0/apiguardian-api-1.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.1.1/opentest4j-1.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.4.2/junit-platform-commons-1.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-params/5.4.2/junit-jupiter-params-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-engine/5.4.2/junit-jupiter-engine-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.4.2/junit-platform-engine-1.4.2.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-engine:5.7.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-engine:1.7.1" level="project" />

모든 의존성을 삭제 후 다시 Junit5만 의존성 추가하여 테스트를 실행하니…

드디어 성공적인 테스트를 완료 할 수 있었다.

여담

project setting에서 분명 모듈 관련된 설정을 모두 삭제 했는데 왜 .iml파일에 설정이 삭제되지 않고 남아있는진 모르겠지만, 필자와 같은 상황이 발생하는 다른 사람들에게 도움이 되길 바라며 글을 마무리한다.

댓글 공유

서론

사실, 제목이 결론이다. @Requestbody Annotation을 사용하려면 반드시 해당 DTO에는 기본 생성자가 명시적으로 존재하여야 한다. 뭔가 설정을 잘못했는지는 모르겠지만 intellij 에서 stacktrace가 출력되지 않아 이 사실을 알 수 가 없어서 엄청나게 삽질을 했기에 내용을 정리한다…


본론

상황

controller 테스트에서 모든 조건을 맞췄는데도 500 에러가 발생하여 테스트를 통과하지 못하는 상황이었는데 @Requestbody annotation을 DTO가 아닌 HashMap으로 변경하니 테스트가 정상적으로 통과되어 JSON이 제대로 DTO로 매핑되지 않아서 발생하는 문제라고 판단했다.

에러 발생

No suitable constructor found for type [simple type, class 클래스명]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)

직역하면 기본 생성자가 존재하지 않아서 에러가 발생했다는 내용이다.

해결 방법

빌더패턴을 사용하던 클래스에 @Noargsconstructor annotation을 사용해서 기본 생성자를 생성하게 하였다.

결론

@Requestbody와 DTO를 매핑되게 해야 할 경우 많은 조건이 필요하다.
이전 포스트에 작성했던 boolean의 변수명이라던가, 지금과 같은 기본 생성자라 라던지… 이게 모두 @Requestbody가 jackson 라이브러리를 이용해 매핑하기 때문이다.


참고 사이트

댓글 공유

서론

연관관계에 있는 객체를 가져와서 set 하는 메서드를 테스트 하는 도중 아래와 같은 에러가 발생하였다.


본론

해당 에러

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
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.jjlee.wedding.payment.domain.Cost.costOptions, no session or session was closed

at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:383)
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:375)
at org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:122)
at org.hibernate.collection.PersistentBag.size(PersistentBag.java:248)
at com.xxxxxx.ImplTest.가져와서_셋해야하는지_테스트(CostServiceImplTest.java:89)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

이 때 디버깅을 해보면 연결된 entitiy에서

1
Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception.

라는 에러가 발생해 있는데 이는 결국 select 한 entitiy가 영속성 컨텍스트 내에 존재하지 않기 때문에 발생한 애러이다.
위 에러에서 말하는 세션이 바로 영속성 컨텍스트를 말하는 것이고, 이는 한 트랜잭션안에 해당 entitiy가 존재하지 않다는 것과 같은 말이다.

해결

이를 해결하기 위해선 현재 지연로딩으로 되어있는 연관관계를 즉시로딩으로 변경하여 한번에 가져오던가,
혹은 Test 메서드에 @Transactional 어노테이션을 줘서 트랜잭션 내에 존재하도록 해주면 테스트가 정상적으로 통과되게 된다.

결과


참고 사이트

댓글 공유

서론

현재 작업 중인 프로젝트에서 테스트 코드를 작성해 테스트할 일이 있었는데 프로젝트의 환경은 spring 4.3에 Junit 4.8이었다.

이에 원래 사용하던 junit5로 넘어갈까 하였으나 junit5를 사용하려면 설정을 spring boot으로 해야 한다는 글들이 있어 같은 테스트 환경을 만들기 위해 junit만 4.12 버전으로 업그레이드한 후 테스트를 진행하였는데 spring 프로젝트지만 config 설정들을 boot 처럼 java 파일로 관리하는 형태여서 java 파일과 properties 파일을 동시에 잡아 줄 필요가 있었는데
@Contextconfiguration(classes = {블라블라…}, locations = {블라블라…})로 잡으니 에러가 발생하여 해결한 방법을 작성해놓는다.


본론

코드

1
2
3
4
5
6
7
@ContextConfiguration(classes = {
DatabaseConfig.class,
SecurityConfig.class,
SocialConfig.class,
EnumConfig.class,
WebMvcConfig.class
}, locations = "classpath:properties/test.properties")

서론에 적은 것 처럼 classes와 locations를 둘 다 설정하였더니 아래와 같이 에러가 발생하였다.

에러

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
java.lang.IllegalArgumentException: Cannot process locations AND classes for context configuration [ContextConfigurationAttributes@64c87930 declaringClass = 'com.xxxx.xxxImplTest', classes = '{class com.xxxx.config.DatabaseConfig, class com.xxx.config.SecurityConfig, class com.xxx.config.SocialConfig, class com.xxx.config.EnumConfig, class com.xxx.config.WebMvcConfig}', locations = '{classpath:properties/test.properties}', inheritLocations = true, initializers = '{}', inheritInitializers = true, name = [null], contextLoaderClass = 'org.springframework.test.context.ContextLoader']: configure one or the other, but not both.

at org.springframework.util.Assert.isTrue(Assert.java:68)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.processContextConfiguration(AbstractDelegatingSmartContextLoader.java:154)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:371)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:305)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildTestContext(AbstractTestContextBootstrapper.java:112)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:120)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:105)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTestContextManager(SpringJUnit4ClassRunner.java:152)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.<init>(SpringJUnit4ClassRunner.java:143)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104)
at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:70)
at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:37)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:70)
at org.junit.internal.requests.ClassRequest.createRunner(ClassRequest.java:28)
at org.junit.internal.requests.MemoizingRequest.getRunner(MemoizingRequest.java:19)
at org.junit.internal.requests.FilterRequest.getRunner(FilterRequest.java:36)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:49)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

해결

@Testpropertysource(classpath:””)로 test properties를 잡아주고
@Contextconfiguration(classes = { XXXConfig.class}) 로 Java config 파일들을 잡아준다.

1
2
3
4
5
6
7
8
@ContextConfiguration(classes = {
DatabaseConfig.class,
SecurityConfig.class,
SocialConfig.class,
EnumConfig.class,
WebMvcConfig.class
})
@Testpropertysource("classpath:properties/test.properties")

댓글 공유

상황

프로덕션 환경에서 Mariadb를 활용하고 있었는데, 프로덕션 환경의 DB로 Repository class를 테스트를 진행하니 테스트 데이터는 롤백되어 DB 남지 않았지만, Auto increment로 지정한 ID가 증가돼서 실제 서비스에서 데이터 저장 시 증가된 ID로 데이터가 저장되는 문제가 발생했다.

해서 Test할 때는 사용 할 데이터베이스 H2를 이용하기로 했다.
H2 데이터베이스는 인메모리 관계형 데이터베이스로 메모리 안에서 실행되기 때문에 어플리케이션을 시작할 때마다 초기화되어 테스트 용도로 많이 사용된다.
하지만 테스트 환경도 프로덕션 환경과 비슷하게 만들어서 테스트 하는 경우에는 테스트환경에도 프로덕션 DB를 생성해서 사용하는 경우도 있다고 한다.

필자가 okky에 올린 질문글
Repository Test시 ID 자동 증가


Test 환경에서 H2 적용

우선 H2 DB를 POM.xml에 추가하여 의존성을 등록한다.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

그 후 Test 환경에서 사용하는 appication.properties에서 데이터베이스 설정을 H2로 설정해준다.

application-test.properties 위치

application-test.properties 내용

1
2
3
4
5
6
7
8
9
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true

spring.jpa.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
  • 프로덕션 환경에서 Mariadb를 사용하기 때문에 dialect를 Mariadb로 설정하고 MODE=Mysql로 설정했다.
  • spring.jpa.hibernate.ddl-auto=none으로 설정하면 시작 시 마다 초기화되지 않기 떄문에 테스트 환경에선 꼭 create-drop으로 설정해준다.

Repository Test class

1
2
3
4
5
6
@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource("classpath:application-test.properties")
class WordRepositoryTest {
...
}
  • JPA Test를 위해 JPA 관련된 설정을 불러오는 @DataJpaTest
  • Test환경에선 프로덕션 환경과 다르게 H2 DB를 사용하므로 H2 DB설정을 지정한 application-test.properties를 호출하기 위한 @TestPropertySource

이 정도로 설정하고 Test하면 정상적으로 H2를 이용한 테스트가 성공할 것이다.


여담

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@BeforeEach
void setUp() {
user = new User();
user.setUsername("jgji");
user.setPassword("qwe123");

userRepository.save(this.user);

user1 = new User();
user.setUsername("haha");
user.setPassword("qwe123");

userRepository.save(this.user1);

List<Word> givenWordList = getWordGiven();
this.word = givenWordList.get(0);
this.word1 = givenWordList.get(1);
}

위 처럼 @Before 메서드를 지정해놓았는데, 각각 Test 메서드를 실행하였을 땐 Auto increment로 지정한 user의 id가 정상적으로 1, 2 이런식으로 생성되었지만 test class 전체로 test를 실행하니 DB가 메서드 마다 각각 실행하고 초기화 되는 것이 아닌지 User Id가 계속 증가해서 테스트가 실패하는 문제가 있었다.

테스트 시 꼭 메서드 각각으로하고 성공한다고 넘어가는게 아니라 클래스 전체로 테스트를 해봐야 할 것 같다.


프로젝트 전체 코드


참고 사이트

댓글 공유

서론

private method도 테스트 해야되는지에 대해선 의견이 많은 것으로 알고 있지만 현재 프로젝트에서 service에서 있는 메서드 중 주요 로직은 private method에 존재하고 public method에선 호출해서 return만 해주는 method가 존재했는데, 이런 경우에 private 메서드를 테스트하기 위한 방법을 정리해둔다.


본론

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
void getPassAndFailWordList_성공() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//given
String[] answerIds = new String[] {"6_1", "4_1", "5_1", "9_3"};
WordService w = new WordServiceImpl(repository, userService);

Method method = w.getClass().getDeclaredMethod("getPassAndFailWordList", String[].class);
method.setAccessible(true);

List<Integer> passWordList = Arrays.asList(6,4,5);
List<Integer> failWordList = Arrays.asList(9);

Object[] obj = new Object[] {answerIds};

//when
Map<String, List<Integer>> map = (Map<String, List<Integer>>) method.invoke(w, obj);

//than
assertThat(map).extracting("pass", String.class)
.contains(passWordList);
assertThat(map).extracting("fail", String.class)
.contains(failWordList);
}
  1. test할 private method가 존재하는 class를 직접 생성
  2. getDeclaredMethod()를 이용해서 해당 클래스에 존재하는 private method를 가져오고
  3. setAccessible()로 private method에 접근을 허용
  4. invoke()로 호출하는데 이 때 invoke()의 매개변수가 Object[] 이므로 원래 호출하려던 private method의 매개변수를 Object[]에 담은 후 Object[]을 매개변수로 넘겨줘야함

참고 사이트

댓글 공유

문제

Test Code

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
@Test
void testCreateWordFail() throws Exception {
// gvien
BindingResult bindingResult = new MapBindingResult(new HashMap(), "");
bindingResult.rejectValue("test", "test", "haha");

WordDTO.AddWord addWord = new WordDTO.AddWord();
addWord.setWord("test");
addWord.setMeaning("테스트");

given(this.service.getCreateWordBindingResult(addWord, bindingResult)).willReturn(bindingResult);

String addWordToJson = getAddWordToJson();

//when
ResultActions action = mockMvc.perform(post("/word/add")
.param("save", "")
.content(addWordToJson)
.contentType(MediaType.APPLICATION_JSON)
.with(csrf()))
.andDo(print());

//then
action.andExpect(status().isOk())
.andExpect(view().name("thymeleaf/word/createWordForm"));
}

위 코드처럼 this.service.getCreateWordBindingResult(addWord, bindingResult) method가 호출될 때 willdReturn으로 지정한 bindingResult가 return되게 하고 하려했으나 given이 제대로 동작하지 않아서 인지 getCreateWordBindingResult method 안에서 result가 존재하지 않아 NullPointerException이 발생하여 테스트가 실패했다.

Test Target

1
2
3
4
5
6
7
8
9
10
11
@PostMapping(value="/word/add", params= {"save"})
public String createWord(@Valid AddWord word, BindingResult bindingResult) {
BindingResult result = service.getCreateWordBindingResult(word, bindingResult);

if (result.hasErrors()) { // NullPointerException 발생
return "thymeleaf/word/createWordForm";
}

service.insertWord(word);
return "thymeleaf/index";
}

해결

ArgumentMatchers class에 any()를 이용해서 파라미터를 넘겼더니 정상적으로 동작했다.

1
given(this.service.getCreateWordBindingResult(any(WordDTO.AddWord.class), any(BindingResult.class))).willReturn(bindingResult);
  • p.s : 기본 자료형 (int, char)이나 String 등은 any()를 사용하지 않고 그대로 파라미터로 받아도 정상적으로 동작하는 것 같은데 어째서 따로 정의한 클래스만 인식(?) 하지 못해서 willReturn이 먹히지 않고 null이 return 되는지 알 수가 없었다.

전체 코드

댓글 공유

문제

Spring Boot + thymeleaf으로 진행하던 프로젝트를 테스트 하던 중
Ajax로 호출하는 Methoed를 Test 하였더니 아래와 같이 403 error가 return 되면서 Test가 실패했다.


원인

thymeleaf를 사용하면 기본적으로 CSRF 토큰을 넘겨주기 때문에 따로 csrf 토큰을 생성하지 않았으나, ajax에선 직접 csrf 토큰을 생성해서 넘겨줘야 하기 때문에 csrf 토큰을 생성해서 넘겨주도록 되어 있는데 Test 시에는 그렇지 않아서 403 Forbidden가 발생한 것으로 보인다.

403

thymeleaf config.html

1
2
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>

common.js

1
2
const token = $("meta[name='_csrf']").attr("content");
const header = $("meta[name='_csrf_header']").attr("content");

해결

mockMvc.perform에서 get, post 호출 시 SecurityMockMvcRequestPostProcessors 의 csrf()with()로 파라미터에 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

...

//when
ResultActions action = mockMvc.perform(post("/word/add")
.param("save", "")
.content(addwordToJson)
.contentType(MediaType.APPLICATION_JSON)
.with(csrf())) // 이 부분
.andDo(print());

...

추가 한 후 다시 테스트 해보면 파라미터에 csrf 토큰이 생성되어 정상적으로 동작하는 것을 확인 할 수 있다.

1
2
3
4
5
6
7
8
9
10
MockHttpServletRequest:
HTTP Method = POST
Request URI = /word/add
Parameters = {save=[], _csrf=[6757e021-c0a3-4390-81d9-9c917d89c8c1]} // 이 부분
Headers = [Content-Type:"application/json", Content-Length:"59"]
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@83a38fdf: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@83a38fdf: Principal: org.springframework.security.core.userdetails.User@31bf3c: Username: jgji; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER}

...

성공


참고 사이트


전체 코드

댓글 공유

평소 Junit의 assertEquals로 알고리즘 코드만 test 하다가
토이 프로젝트에서도 Junit을 사용해보기 위해 Spring에서의 Junit Test 방법을 정리 해보자 한다


Test할 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class HomeController {

@GetMapping("/")
public String home(Locale locale, Model model) {
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "home";
}
}
  • index 페이지로 매핑되는 home 메서드만 존재하는 Controller

standaloneSetup() 메서드를 활용한 Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HomeControllerTest {

private MockMvc mockMvc;

@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new HomeController()).build();
}

@Test
public void homeTest() throws Exception {

mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("serverTime"))
.andExpect(view().name("home"));
}
}

MockMvc 클래스를 통해 HTTP GET, POST 등에 대한 테스트를 할 수 있게 한다

  • perform() : get 방식으로 url을 호출
  • status() : http status에 대해 테스트
  • model() : model에 “serverTime” attribute가 존재하는 지 확인
  • view() : return하는 view의 name이 “home”인지 확인

위 메서드들을 정상적으로 사용하기 위해선 아래 처럼 메서드들을 import 시켜야한다

1
2
3
4
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

webAppContextSetup() 메서드를 활용한 Test

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
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"
, "file:src/main/webapp/WEB-INF/spring/root-context.xml"
, "file:src/main/webapp/WEB-INF/spring/spring-security.xml"})
@WebAppConfiguration
public class HomeControllerTest {

@Autowired
private WebApplicationContext context;

private MockMvc mockMvc;

@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}

@Test
public void homeTest() throws Exception {

mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("serverTime"))
.andExpect(view().name("home"));
}
}

@RunWith

  • Junit에 내장된 Runner외에 다른 실행자를 실행시킨다
  • 위 코드에선 SpringRunner 실행자로 Test를 실행하는데 이때 SpringRunner class는 SpringJUnit4ClassRunner class를 상속받고 있다
  • 테스트를 진행할 때 ApplicationContext를 만들고 관리하는데 이 때 싱글톤을 보장한다

@ContextConfiguration

  • 스프링 Bean 설정 파일의 위치를 지정한다
  • 위 코드에서는 Controller는 servlet-context.xml, 그 외 Service, Repository 등은 root-context.xml, 암호화를 위한 bcryptPasswordEncoder bean은 spring-security.xml에 지정되어 있어 3개를 지정했다

@WebAppConfiguration

  • applicationContext의 웹 버전을 작성하는데 사용된다

standalonesetup과 webAppContextSetup의 차이

standalonesetup() 메서드는 테스트할 컨트롤러를 수동으로 초기화해서 주입하고, webAppContextSetup() 메서드는 스프링의 ApplicationContext의 인스턴스로 동작한다

standalonesetup() 메서드는 컨트롤러에 집중하여 테스트할 때 사용되고
webAppContextSetup() 메서드는 스프링의 DI 컨테이너를 이용해 스프링 MVC 동작을 재현해서 테스트한다


번외 java.lang.NoClassDefFoundError: javax/servlet/SessionCookieConfig 에러

1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.NoClassDefFoundError: javax/servlet/SessionCookieConfig
at org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder.initWebAppContext(StandaloneMockMvcBuilder.java:339)
at org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder.build(AbstractMockMvcBuilder.java:139)
at com.toon.controller.HomeControllerTest.setUp(HomeControllerTest.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
... 26 more
  • Spring 4.0 이상에서 Junit으로 테스트 시 servlet 3.0 API를 사용하기 때문에 발생
  • pom.xml에서 servlet 버전을 3.0.1 이상으로 올려주면 해결
  • artifactId가 javax.servlet-api 으로 변경 됐으므로 함께 변경해준다.
1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

참고 사이트

댓글 공유

  • page 1 of 1

Junggu Ji

author.bio


author.job