본문 바로가기

우테코 프리코스

프리코스 2주차 미션 (2)

2주차 미션은 1주차 미션과는 다르게 테스트가 요구 사항으로 추가되었다.

 

< 추가된 요구 사항 >

 

첫 테스트 코드 작성이기도 해서 테스트 코드만으로 글 작성을 해보려 한다.

 

처음에는 그저 막막했다. 테스트 코드를 작성해본 적도 없었고 이를 배운 후 작성하는게 아닌 스스로 공부해서 작성해야 한다는게 부담으로 다가왔다. 그래도 며칠이라는 시간이 있었으니 한번 열심히 해보기로 했다. 요구 사항에 명시된대로 test/java/study 를 딱 열어봤지만 무지한 상태에서 봐서 그런지 이해가 가질 않았다. 일단 .JUnit 5와 AssertJ를 공부하기로 마음 먹었다. 

먼저 JUnit 5와 AssertJ에 앞서서 테스트 코드는 기본적으로 아래의 단계를 만족하도록 작성할 수 있다. 초심자라면 더더욱 이것을 따르는게 좋다고 한다.

 

  1. given : 어떤 상황이 주어졌을 때 (이 데이터를 기반으로 함)
  2. when : ~ 를 실행했을 때 (검증할 것을 실행)
  3. then : 검증한 결과가 ~ 가 나와야함

1, 2 단계는 실행 단계, 3 단계는 검증 단계라고 할 수 있다.

실행 단계에서는 사용자 입력을 모방해 테스트할 로직을 실행시키고

검증 단계에서는 실행 단계가 예상된 대로 동작하는지 검증한다.

JUnit이란

Junit은 자바에서 많이 사용되는 유닛 테스트 프레임워크이다.

  • 단정(assert) 메소드로 테스트 케이스의 수행 결과를 판결한다.
  • JUnit4부터는 어노테이션을 통해 개발자가 간결하게 사용할 수 있도록 지원하고 있다.
  • 포스팅하고자 하는 Junit5는 JAVA8 버전 이상부터 사용 가능하며 스프링 부트 2.2버전 이상부터는 기본으로 제공하고 있다.
  • 테스트 결과를 확인하는 것 이외에도 최적화된 코드를 유추해내는 기능도 제공한다.

JUnit5의 중요 어노테이션

 

JUnit5 어노테이션 설명 JUnit4 어노테이션
@Test 테스트 method임을 선언한다. @Test
@ParameterizedTest 매개변수를 받는 테스트를 작성할 수 있다.  
@RepeatedTest 반복되는 테스트를 작성할 수 있다.  
@TestFactory @Test 어노테이션을 통해 선언된 정적 테스트가 아닌 동적으로 테스트를 사용한다.  
@TestTemplate 공급자에 의해 여러 번 호출될 수 있도록 설계된 테스트 케이스 템플릿을 의미한다.  
@TestMethodOrder 테스트 메소드 실행 순서를 구성하는데 사용된다.  
@DisplayName 테스트 클래스 또는 메소드의 사용자 정의 이름을 선언할 수 있다.  
@DisplayNameGeneration 이름 생성기를 선언할 수 있다. (ex. '_'를 공백문자로 치환해주는 생성기: new_test --> new test)  
@BeforeEach 모든 테스트 실행 전에 수행할 테스트에 사용한다. @Before
@AfterEach 모든 테스트 실행 후에 수행할 테스트에 사용한다. @After
@BeforeAll 클래스를 실행하기 전 제일 먼저 실행할 테스트 작성하는데, static로 선언한다. @BeforeClass
@AfterAll 현재 클래스 종료 후 해당 테스트를 실행하는데, static으로 선언한다. @AfterClass
@Nested 정적이 아닌 중첩 테스트 클래스임을 나타낸다.  
@Tag 클래스 또는 메소드 레벨에서 태그를 선언할 때 사용한다. (메이븐을 사용할 경우 설정에서 테스트를 태그를 인식해 포함하거나 제외시킬 수 있다.)  
@Disabled 사용하지 않음을 표시한다. @Ignore
@Timeout 테스트 실행 시간을 설정할 수 있는 어노테이션으로, 설정한 시간을 초과하면 테스트를 실패한 것으로 간주한다.  
@ExtendWith 확장을 선언적으로 등록할 때 사용한다.  
@RegisterExtension 필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용한다.  
@TempDir 필드 주입 또는 매개변수 주입을 통해 임시 디렉토리를 제공하는데 사용한다.  

 

Jnuit5.Assertions 안에는 테스트 검증을 위한 다양한 메서드가 존재하지만 흔히들 구리(?)다고 한다. 함수 체이닝이 안되서 읽기도 힘들고, IDE가 자동완성도 지원하지 않는다. 그래서 보통 서드파티 라이브러리(AssertJ.Asesertions)를 대신 사용한다. 테스트 코드 작성시 아무래도 Junit5보다 가독성과 편의성을 높여주는 라이브러리다.

Hamcrest, Truth라고 JUnit5와 호환 가능한 Third-Party Assertion 라이브러리들이 존재하지만 보통 제일 직관적이고 편한 AssertJ를 많이 사용한다. 

 

assertThat() 으로 시작하며, 형식은 아래와 같다.

 

@Test
void a_few_simple_assertions() {
    assertThat("The Lord of the Rings").isNotNull()
                                        .startsWith("The")
                                        .contains("Lord")
                                        .endsWith("Rings");
}

 

물론 아래와 같이 자바의 함수형 인터페이스를 이용해 여러 테스트를 하나로 묶어서 실행할 수 있는 assertAll이 존재하지만 그 외에는 되도록이면 assertThat()으로 통일해서 간결하게 쓰면 된다.

 

@Test
void test1() {
    assertAll(
        //A Case
        () -> assertFalse(true, "Exception!!!"),
 
        //B Case
        () -> {
            TestObject tObj = null;
            assertNotNull(tObj, "Object is Null!!");
        }
    );
}

 

 

아래는 Assert의 메서드다.

 

  • 문자열 테스트 예제
public class Test {
    @Test
    void add() {
        String s = assertThat("Java Test Success.")   // 주어진 "Java Test Success." 라는 문자열은
                .isNotEmpty()   // 비어있지 않고
                .isNotNull() // null 이 아니며
                    
                .startsWith("Jav") // "Jav"로 시작하고
                .endsWith("s.") // "s."로 끝나며
                    
                .doesNotContain("aaa")  // "aaa"는 포함하지 않으며
                .contains("Java")   // "Java"를 포함하고
                .contains("Success")  // "Success"도 포함하며
                    
	            // "Java Test Success." 와 equals() 메서드로 비교시 true 이고 (데이터 비교)
                .isEqualTo(new String("Java Test Success."))
                .isNotEqualTo("ggg")  // "ggg" 와 equals() 메서드로 비교시 false 이며 (데이터 비교)
                    
                .isSameAs("Java Test Success.") // "Java Test Success." == "Java Test Success." 가 true 이고 (주소값 비교)
	            // "Java Test Success." == new String("Java Test Success.") 가 false 이며 (주소값 비교)
                .isNotSameAs(new String("Java Test Success.")) 
                
                .isInstanceOf(String.class) // String 클래스이고
                .isInstanceOf(CharSequence.class) // String 이 구현한 CharSequence 인터페이스이기도 하고
                .isNotInstanceOf(Character.class) // Character 클래스는 아니며
                    
                .isInstanceOfAny(String.class, Character.class) // String 클래스이거나 또는 Character 클래스이고
                .isNotInstanceOfAny(Character.class, Integer.class) // Character 클래스가 아니고 Integer 클래스가 아니며
                    
                .isExactlyInstanceOf(String.class) // '정확하게는' String 클래스이고
                .isNotExactlyInstanceOf(CharSequence.class) // '정확하게' CharSequence 인터페이스는 아니며
                    
                .toString(); // getClass().getName() + '@' + Integer.toHexString(hashCode()) 를 반환한다.
            
        System.out.println(s);
    }
}

 

 

  • 숫자 테스트 예제
public class Test {
    @Test
    void add() {
        String s = assertThat(8.67)   // 주어진 8.67 이라는 숫자는
                .isBetween(8.67, 9d) // 8.67 이상 9 이하이고
                .isStrictlyBetween(8.66, 9d) // 8.66 초과 9 미만이며
                
                .isCloseTo(8, offset(0.67d)) // 8과의 오차범위가 0.67 이내이고
                .isCloseTo(8, withPercentage(10)) // 8과의 오차범위가 10퍼센트 이내이며
                
                .isNotCloseTo(8, offset(0.66d)) // 8과의 오차범위가 0.66 초과이고
                .isNotCloseTo(8, withPercentage(1)) // 8과의 오차범위가 1퍼센트 초과이며
                
                .isGreaterThan(8)   // 8보다 크고
                .isLessThan(9)  // 9보다 작으며
                
                .isPositive()   // 양수이고
                .isNotNegative() // 음수가 아니고
                .isNotZero() // 0이 아니며
                
                .isFinite() // 유한한 숫자이고
                .isNotNaN() // NAN 이 아니며
                
                .isEqualTo(8, offset(1d))  // 8과의 차이가 오차범위 1 이내이고
                .isEqualTo(8.6, offset(0.1d))  // 8.6과의 차이가 오차범위 0.1 이내이며
                .isEqualTo(8.67)   // 오프셋 없이는 8.67와 같습니다
                
                .toString(); // getClass().getName() + '@' + Integer.toHexString(hashCode()) 를 반환한다
        
        System.out.println(s);
    }
}

 

위와 같이 어느정도 기본적인 사항들을 익히고 test/java/study 파일을 들여다보니 전과는 다르게 이해가 가기 시작했다. 조금씩 테스트 코드를 작성하기 시작했고 무사히 실행되는 것을 확인했을 때는 정말 좋았다. 그런데 역시 좋은 일이 있으면 나쁜 일도 있기 마련인 것인지, 도무지 해결되지 않는 오류에 직면했다. 엄청난 시간을 들여서 오류를 해결했고 덕분에(?) 의도치않은 공부까지 했다. 이 오류 해결에 대해 글을 써놓고 다음부터 같은 오류를 범하지 않기로 다짐했다. 이와 관련된 글은 현 블로그 내에 있으며 아래의 링크를 누르면 바로 들어가 볼 수 있다.

https://golden-retriever.tistory.com/6

 

테스트 코드 : NoSuchElementException 오류

package racingcar.view; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import java.io.ByteArrayInputStream; import org.junit.jupiter.api.Test; class InputViewTest { private InputView inputView; @Test void inputCarNames() { // given

golden-retriever.tistory.com

 

앞 글에서 전체 코드에 대해 링크를 첨부했지만 번거롭지 않게 한번 더 첨부하는게 나을 듯하여 아래와 같이 첨부한다.

https://github.com/JaeMin-1/java-racingcar-6/tree/JaeMin-1

 

GitHub - JaeMin-1/java-racingcar-6

Contribute to JaeMin-1/java-racingcar-6 development by creating an account on GitHub.

github.com

마치며

구현 자체는 크게 어렵지 않아서 미션이 거의 나오자마자 당일날 완료했다. 그러나 이제 1주차 숫자 야구 때의 코드 리뷰를 통해 여러 분들의 코드를 보며 느꼈고, 또 피드백도 받고 하면서 MVC 패턴을 신경쓰게 되었고 코드 리펙토링에도 더욱 신경을 쓰게 되었다. 거기다 새로운 요구 사항인 테스트 코드까지 생각하다보니 정말 일주일을 다 쏟아부었다. 그래도 살짝은 성장했는지 1주차때는 고민해보지 않았던 부분에 대해서 고민해보게 되었다. static 정적 메서드 접근이 좋을지, 객체 생성 접근이 좋을지, 또는 객체 생성 시 외부에서 생성된 객체를 가져와 쓰는게 좋을지, 내부에서 새로 생성해서 쓰는게 좋을지, 그리고 평소 그냥 당연하게 쓰던 this의 정확한 쓰임은 뭔지, 또한 private 메서드는 왜 테스트코드에서 지양되는지 등 계속 스스로 물어보며 생각하고 찾아봤다. 많은 정보가 인터넷 상에 존재했지만 정확히 내것으로 받아들이기엔 아직 부족함이 많다고 느꼈다. 무엇보다 제 코드에 적용하려고 하다보니 무엇이 더 좋은 건지 감이 잘 잡히지 않았다. 리펙토링을 다 해놓고 보면 꽤 괜찮은 코드같은데 또 이러면 어떨까 저러면 어떨까 계속 변경해보며 시간을 보냈다. 최종적으로 제출한 코드도 전문가나 잘하시는 분이 옆에 딱 붙어서 멘토로서 하나하나 지적해줬으면 하는 생각이 든다. 그리고 테스트 코드는 처음 작성하다보니 감이 잘 잡히지 않았다. 차근히 공부해가며 작성을 했지만 입력값에 대한 테스트를 할때 NoSuchElementException 오류는 정말 엄청난 시간을 들이게 만들었다. 이 오류 하나에 대한 블로그 글까지 작성했으며, 이 오류 덕분이라 해야할지, console 파일도 상세히 이해해보고 scanner, system.in 등 기본 입력에 대해 공부를 하게됐다. 정말 일주일이라는 시간을 온전히 2주차 미션에 쏟아부었다. 많은 시간을 쏟아부으면서 성장하고 있다는 것에 대한 뿌듯함도 느꼈지만 한편으로는 아직 스스로 너무 부족하다는 것을 느꼈다. 이해가 잘 가지않는 부분도 많았고 실제로 코드에 적용해보는 것에 어려움을 느꼈으며, 디스코드 채널에서 다른 분들이 토론하는 것에 대해 감히 끼지를 못하겠다는 생각이 들기도 했다. 솔직히는 스스로 많이 작다고 느끼기도 했다. 하지만 잘하시는 분들은 잘하시는 분들이고, 나는 느리더라도 꾸준하게 성장하기로 마음 먹었다. 실력으로 순위를 매긴다면 나는 우테코에 들어갈 수 없겠지만 우테코는 엄연히 교육 프로그램, 배우고자 하는 의지, 그걸 보여주는 프리코스에서의 성장하는 모습과 가능성을 잘 보여줄 수만 있다면 나도 충분히 들어갈 수 있다고 생각한다. 남은 3, 4주차 더욱 몰입해서 성장을 이끌어 내고 최종 코테까지 통과해 당당하게 우테코 6기에 합류할 의지를 다지면서 3주차 미션을 기다리겠다.

 

 

 

글 작성에 도움을 주신 목록 ->

https://jiwondev.tistory.com/186

https://hseungyeon.tistory.com/328

https://tjdtls690.github.io/studycontents/java/2022-08-02-assertj01/

https://velog.io/@chaerim1001/Java-JUnit5-%EA%B8%B0%EC%B4%88-%EC%A0%95%EB%A6%AC

'우테코 프리코스' 카테고리의 다른 글