포스트

스프링 부트3 제 13장

이 코드는 괜찮나? 저 코드는 문제 없으려나? 걱정이 백만 개! ㅠㅠ

스프링 부트3 제 13장

테스트(Test)

프로그램의 품질을 검증하는 것으로, 의도대로 프로그램이 잘 동작하는지 확인하는 과정이다.
테스트 도구를 활용해 코드를 검증한다는 것은 테스트 코드(test code)를 작성해 실행한다는 말이다. 테스트 코드는 보통 다음 3단계로 작성한다.

  1. 예상 데이터 작성
  2. 실제 데이터 획득
  3. 둘을 비교해 검증

작성한 코드가 테스트를 통과하면 지속적인 리팩터링으로 코드를 개선한다. 만약 테스트를 통과하지 못하면, 디버깅(debugging)을 해야 한다.

테스트 코드는 다양한 경우를 대비해 작성하는데, 이를 테스트 케이스(test case)라고 한다. 테스트 케이스는 ‘성공할 경우’뿐만 아니라 ‘실패할 경우’도 고려해야 한다.

테스트 주도 개발(TDD, Test Driven Development)란 일단 테스트 코드를 만든 후 이를 통과하는 최소한의 코드부터 시작해 점진적으로 코드를 개선 및 확장해 나가는 개발 방식이다.

테스트 코드 작성하기

지난 장에서 만든 ArticleService를 테스트하는 테스트 코드를 작성하겠다.

IntelliJ에서 테스트 코드를 만들려면 해당 메서드 우클릭 -> 생성 -> 테스트를 하면 된다.
이러면 main 디렉토리 밑에 test 디렉토리가 만들어지는데, main > java 디렉토리와 똑같이 test > java 디렉토리로 생성된다.

FirstprojectApplicationTests

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@SpringBootTest // 해당 클래스를 스프링 부트와 연동해 테스트
class ArticleServiceTest {
    @Autowired // 외부 객체 주입
    private ArticleService articleService; // 테스트하려는 객체 선언
    
    @Test
    void index() {
        // 1. 예상 데이터 작성
        Article a = new Article(1L, "aaaa", "1111");
        Article b = new Article(2L, "bbbb", "2222");
        Article c = new Article(3L, "cccc", "3333");
        List<Article> expected = new ArrayList<Article>(Arrays.asList(a, b, c));

        // 2. 실제 데이터 획득
        List<Article> articles = articleService.index();

        // 3. 예상 데이터와 실제 데이터 비교해 검증
        assertEquals(expected.toString(), articles.toString());
    }

    @Test
    void show_success() { // 게시글 조회 성공 (존재하는 id 입력)
        // 1. 예상 데이터
        Long id = 1L;
        Article expected = new Article(id, "aaaa", "1111");

        // 2. 실제 데이터
        Article article = articleService.show(id);

        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    void show_fail() { // 게시글 조회 실패 (존재하지 않는 id 입력)
        // 1. 예상 데이터
        Long id = -1L;
        Article expected = null;

        // 2. 실제 데이터
        Article article = articleService.show(id);

        // 3. 비교 및 검증
        assertEquals(expected, article);
    }

    @Test
    @Transactional
    void create_success() { // 게시글 생성 성공 (title과 content만 있는 dto 입력)
        // 1. 예상 데이터
        String title = "dddd";
        String content = "4444";
        ArticleForm dto = new ArticleForm(null, title, content);
        Article expected = new Article(4L, title, content);
        
        // 2. 실제 데이터
        Article article = articleService.create(dto);
        
        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    @Transactional
    void create_fail() { // 게시글 생성 실패 (id가 포함된 dto 입력)
        // 1. 예상 데이터
        Long id = 4L;
        String title = "dddd";
        String content = "4444";
        ArticleForm dto = new ArticleForm(id, title, content);
        Article expected = null; // id가 있으면 Article은 null을 반환

        // 2. 실제 데이터
        Article article = articleService.create(dto);

        // 3. 비교 및 검증
        assertEquals(expected, article);
    }

    @Test
    @Transactional
    void update_success1() { // 게시글 수정 성공 (존재하는 id와 title, content가 있는 dto 입력)
        // 1. 예상 데이터
        Long id = 1L;
        String title = "수정된 제목";
        String content = "수정된 내용";

        ArticleForm dto = new ArticleForm(id, title, content);
        Article expected = new Article(id, title, content);
        
        // 2. 실제 데이터
        Article article = articleService.update(id, dto);
        
        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    @Transactional
    void update_success2() { // 게시글 수정 성공 (존재하는 id와 title이 있는 dto 입력)
        // 1. 예상 데이터
        Long id = 1L;
        String title = "수정된 제목";
        String content = "1111";
        ArticleForm dto = new ArticleForm(id, title, content);
        Article expected = new Article(id, title, content);

        // 2. 실제 데이터
        Article article = articleService.update(id, dto);

        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    @Transactional
    void update_fail() { // 게시글 수정 성공 (존재하지 않는 id의 dto 입력)
        // 1. 예상 데이터
        Long id = -1L;
        String title = "수정된 제목";
        String content = "수정된 내용";
        ArticleForm dto = new ArticleForm(id, title, content);
        Article expected = null;

        // 2. 실제 데이터
        Article article = articleService.update(id, dto);

        // 3. 비교 및 검증
        assertEquals(expected, article);
    }

    @Test
    @Transactional
    void delete_success() { // 게시글 삭제 성공 (존재하는 id 입력)
        // 1. 예상 데이터
        Long id = 1L;
        Article expected = new Article(id, "aaaa", "1111");

        // 2. 실제 데이터
        Article article = articleService.delete(id);

        // 3. 비교 및 검증
        assertEquals(expected.toString(), article.toString());
    }

    @Test
    @Transactional
    void delete_fail() { // 게시글 수정 성공 (존재하지 않는 id의 dto 입력)
        // 1. 예상 데이터
        Long id = -1L;
        Article expected = null;

        // 2. 실제 데이터
        Article article = articleService.delete(id);

        // 3. 비교 및 검증
        assertEquals(expected, article);
    }
}

@SpringBootTest는 해당 클래스를 스프링 부트와 연동해 통합 테스트를 수행할 수 있게 해주는 어노테이션이다.

@Test는 해당 메서드가 테스트를 위한 코드라고 선언하는 어노테이션이다.

테스트 시 데이터 조작하는 행위(생성, 수정, 삭제)를 한다면 테스트를 마치고 롤백을 해야 한다. 롤백하지 않으면 전의 테스트에서 조작된 데이터가 문제를 일으킬 수 있다. 따라서 데이터를 조작하는 메서드(create(), update(), delete())에는 @Transactional 어노테이션을 추가해야 한다.

참고로 @Transactional 어노테이션은 일반적인 경우엔 예외 상황에서만 롤백되지만, 테스트 상황에선 테스트가 성공하든 실패하든 상관없이 매번 롤백된다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.