An introduction to unit testing

๐Ÿ“ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

  1. mickito & test ์˜์กด์„ฑ ์ถ”๊ฐ€
  2. ํ…Œ์ŠคํŠธํ•  ํ•จ์ˆ˜ ์ƒ์„ฑํ•˜๊ธฐ
  3. http.Client Mock ๊ฐ์ฒด์™€ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ƒ์„ฑํ•˜๊ธฐ
  4. ๊ฐ ์กฐ๊ฑด๋งˆ๋‹ค ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ
  5. ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ํ•˜๊ธฐ

1. ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป mockito์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ

mockito ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด pubspec.yaml ํŒŒ์ผ์˜ dev_dependencies ์˜์—ญ์— flutter_test ์˜์กด์„ฑ๊ณผ ํ•จ๊ป˜ ์ถ”๊ฐ€ํ•œ๋‹ค.

๐Ÿš€ http ํŒจํ‚ค์ง€๋„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— dependencies์˜์—ญ์— ์ถ”๊ฐ€ํ•œ๋‹ค.

dependencies:
  http: <newest_version>
dev_dependencies:
  test: <newest_version>
  mockito: <newest_version>

Tip : Flutter version์— ๋”ฐ๋ผ test์˜ ๋ฒ„์ „์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Œ.
์ €๋Š” ํ˜„์žฌ 1.17.3์˜ Flutter stable version์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ณ , test๋Š” 1.14.4๋ฒ„์ „์ด๋‹ค.

http: ^0.12.2
...
test: ^1.14.4
mockito: ^4.1.1

2. ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป ํ…Œ์ŠคํŠธํ•  ํ•จ์ˆ˜ ์ƒ์„ฑํ•˜๊ธฐ

๋ณธ ์˜ˆ์ œ์—์„œ๋Š” ์ธํ„ฐ๋„ท์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ˆ์ œ์˜ fetchPost ํ•จ์ˆ˜๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธํ•œ๋‹ค. ์ด ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ๋‘๊ฐ€์ง€ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•˜๋‹ค.

  1. ํ•จ์ˆ˜์— http.Client๋ฅผ ์ œ๊ณตํ•ด์•ผํ•œ๋‹ค.
  2. mock ๊ฐ์ฒด๋กœ ๋งŒ๋“ค๊ธฐ ์–ด๋ ค์šด static http.get() method๊ฐ€ ์•„๋‹Œ, ์ œ๊ณต๋œ client๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

๐Ÿ’ขPost๋ผ๋Š” class๋ฅผ ๋ฏธ๋ฆฌ ์ •์˜ํ•ด๋‘์–ด์•ผ ํ•œ๋‹ค. ์ธํ„ฐ๋„ท์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ์— ๋ณด๋ฉด ๋‚˜์˜ด.

Future<Post> fetchPost(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/posts/1');

  if (response.statusCode == 200) {
    // ๋งŒ์•ฝ ์„œ๋ฒ„๋กœ์˜ ์š”์ฒญ์ด ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด, JSON์œผ๋กœ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
    return Post.fromJson(json.decode(response.body));
  } else {
    // ๋งŒ์•ฝ ์š”์ฒญ์ด ์‹คํŒจํ•˜๊ฒŒ ๋˜๋ฉด, ์—๋Ÿฌ๋ฅผ ๋˜์ง‘๋‹ˆ๋‹ค.
    throw Exception('Failed to load post');
  }
}

โœ… https://jsonplaceholder.typicode.com/posts/1 ์˜ ๊ฒฐ๊ณผ๊ฐ’ :
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

3. ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป http.Client Mock ๊ฐ์ฒด์™€ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ƒ์„ฑํ•˜๊ธฐ

์ด์ œ MockClient ํด๋ž˜์Šค์™€ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค ์ฐจ๋ก€์ด๋‹ค. ๋‹จ์œ„ํ…Œ์ŠคํŠธ ์†Œ๊ฐœ ์˜ˆ์ œ๋ฅผ ๋”ฐ๋ผ ์ตœ์ƒ์œ„ test ํด๋” ์•ˆ์— fetch_post_test.dart ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.

MockClient ํด๋ž˜์Šค์—์„œ๋Š” http.Client ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•  ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ํ†ตํ•ด MockClient๋ฅผ fetchPost ํ•จ์ˆ˜์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉฐ, ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๋‹ค๋ฅธ http ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
// Mockito ํŒจํ‚ค์ง€์—์„œ ์ œ๊ณต๋˜๋Š” Mock ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MockClient๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
// ๊ฐ ํ…Œ์ŠคํŠธ ๋งˆ๋‹ค ์ด ํด๋ž˜์Šค์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.
class MockClient extends Mock implements http.Client {}

main() {
  // ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค.
}

4. ๊ฐ ์กฐ๊ฑด๋งˆ๋‹ค ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์œ„์—์„œ ์ž‘์„ฑํ•œ fetchPost() ํ•จ์ˆ˜๋Š” ๋‘ ๊ฐ€์ง€ ์ผ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค.
1. http ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋ฉด Post๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
2. http ์š”์ฒญ์ด ์‹คํŒจํ•˜๋ฉด Exception์„ ๋˜์ง„๋‹ค.

๋”ฐ๋ผ์„œ ๋‘๊ฐ€์ง€ ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค. MockClient ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๊ณต ์ผ€์ด์Šค๋ฅผ ์œ„ํ•ด OK ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์‹คํŒจ ์ผ€์ด์Šค๋ฅผ ์œ„ํ•ด Error ์‘๋‹ต ๋ฐ˜ํ™˜์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ์กฐ๊ฑด์€ Mockito๊ฐ€ ์ œ๊ณตํ•˜๋Š” when() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋‹ค.

main() {
  group('fetchPost', () {
    test('returns a Post if the http call completes successfully', () async {
      final client = MockClient();

      // ์ œ๊ณต๋œ http.Client๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ, ์„ฑ๊ณต์ ์ธ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด 
      // Mockito๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('{"title": "Test"}', 200));

      expect(await fetchPost(client), isInstanceOf<Post>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockClient();

      // ์ œ๊ณต๋œ http.Client๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ, ์‹คํŒจ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด 
      // Mockito๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
      when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchPost(client), throwsException);
    });
  });
}

5. ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ํ•˜๊ธฐ

IDE๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, run ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ณ 
editor๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด command line์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๊ฐ€๋Šฅํ•˜๋‹ค.

flutter test ํŒŒ์ผ๊ฒฝ๋กœ
ex) $flutter test test/unit_test/http_test_fetchPost.dart

0๊ฐœ์˜ ๋Œ“๊ธ€