[비기너 가이트 투 테스팅] 플러터 테스트에 대한 최소한의 가이드 (모킹)

김영진·2023년 4월 10일
0

목적

테스트 공부

내용

테스트를 작성하기 전에 테스트를 모의하는 방법을 배워야 한다고 생각하는데, 이는 우리가 많이 사용해야 하는 중요한 주제 입니다!

기본적으로 우리는 2가지 패키지를 플러터에서 선택할 수 있습니다.

  1. Mockito
  2. Mocktail

둘다 대단한 패키지다, 하지만 나는 mocktail을 사용할 예정

Mocktail을 사용한 이유는?

모키토도 훌륭하고 다트 팀 자체에서 개발했다! 하지만 나는 목테일의 접근 방식이 마음에 들었습니다. 왜냐면
1. 간단한 API
2. 노 코드 생성
3. 타입 안정성
단지 이러한 이유!

패키지가 필요한 이유는 무엇인가요? 패키지 없이 작성할 수 없나요?

패키지를 사용하면 코드량이 비약적으로 줄어듬

모킹이란 무엇이며 왜 필요한가요?

"단위 테스트의 기본 개념은 외부 종속 요소의 동작이 아닌 현재 테스트 중인 단위를 분리하고 집중하는 것입니다. 하지만 대부분의 경우 DB, WebServer, PlatformApi, external device 등과 같은 외부 종속성에 의존해야 합니다.

현재 단위가 웹 API에 종속되어 있다고 가정해 보자. 서버가 라이브 상태일때는 테스트가 느리지만 정상적으로 장동합니다. 하지만 서버가 오프라인 상태일때는 단위테스트가 실패합니다. 이렇게 하면 단위 테스트를 예측할 수 없게 됩니다. 웹 서버는 우리가 통제할 수 없기 때문입니다. 웹 서버가 다운되는것은 우리의 잘못이 아닙니다. 이것이 바로 모킹이 들어오는곳 입니다.

따라서 다른 종속성으로부터 테스트를 격리하기 위해 가짜 서비스를 생성하고 모든 외부 종속성이 제대로 작동한다고 가정합니다.

기본적으로 종속성을 원본으로 대체/가짜 로 만들수 있다고 생각할 수 있다.

모킹 대신 페이크 서비스라고 부르면 어떨까요?

사실 주된 이유는 모르겠지만, 페이크 서비스는 다른 의미로 쓰이는것 같아요.(가짜 환경에서 일하는 것처럼) 조롱 대신 페이크 서비스를 사용하면 용어가 혼동될 수 있습니다. 용어가 혼동될것 같아서요!

또한 앱을 테스트하는동안 "스텁"이라는 용어를 많이 보게 될 것입니다!

스텁이란 무엇인가요?

스텁은 다른 프로그래밍 기능을 대신하는데 사용되는 코드 조각입니다 스텁은 기존 코드의 동작을 시뮬레이션할 수 있습니다.

기본적으로 클래스를 가져와서 똑같이 보이지만 다른 기능을 가진 가짜 클래스를 만듭니다

class Horse {}
class Trojan extends Mock implements Horse {}

이론은 충분하셨나요?

그럼 이제 우리의 숨겨진 능력을 살펴볼까요!

로직은 간단합니다!

트로이 목마에게 모든 경우에 무엇을 해야하는지 말하세요!
실행하세요!
트로이 목마가 일을 제대로 수행했는지 여부를 제어하세요

// `sound` 메서드의 스텁
when(() => trojan.sound()).thenReturn('horse sound');

// 상호작용이 발생하지 않았는지 확인합니다
verifyNever(() => trojan.sound());

// 모의 트로이 목마 인스턴스와 상호작용 합니다
trojan.sound();

// 상호작용이 발생했는지 확인합니다.
verify(() => trojan.sound()).called(1);

// 모의 인스턴스와 다시 상호작용합니다
trojan.sound();

// 상호작용이 두번 발생했는지 확인합니다.
verify(() => trojan.sound()).called(2);

사실 테스트에는 크게 3가지 단계가 있습니다!
(init, dispose 단계는 잠시 잊어두자)

  1. Arrange - 행동을 결정하세요

    when : 테스트하고자하는 메서드가 실행되면 이렇게 해! 라는 함수

// 동기 메서드
when(() => service.syncMethod()).thenReturn('ehe');

// 비동기 메서드
when(() => service.asyncMethod()).thenAnswer(() async => 'ehe');

// 에러 핸들링
when(() => service.brokenMethod()).thenThrow(Exception('ehe'));
  1. Act - 사용해보세요!
    여기에는 특별한 경우가 없으며 메서드를 싱행하기만 하면 됩니다! 만약 니가 필요하다면!
final res = service.syncMethod();
final res2 = await service.asyncMethod();
final res3 = service.brokenMethod();
  1. Assert - 확인해라
    마지막으로, 제대로 작동하는지 확인합니다! 따라서 기대치를 정의하고 이를 결과와 비교합니다!
// 해당 메서드가 이전에 실행된 적이 없는지 확인합니다
verifyNever(() => service.syncMethod());

// 한번 호출하고 앱에 한번만 호출되었는지 물어봅니다
verify(() => service.syncMethod()).called(1);

// 실행할 것으로 예상하지만 이 값을 반환하나요?
expect(service.syncMethod(), 'ehe');

// 기대값을 던질거라는건 알지만 정말 던지나요?
expect(() => service.brokenMethod(), throwsA(isA<Exception>()));

원할때 언제든지 서비스를 쉽게 재설정할 수도 있습니다.

다음 테스트를 쉽게 시작할수도 있습니다.

reset(service);

주의!!!

사용자 정의 타입을 인자 일치자와 함께 사용하러면 처음에 타입을 등록해야 합니다! 목테일은 기본적으로 원시 타입만 지원하기 때문입니다! 하지만 걱정하지 마세요. 매우 쉽습니다!

class Food {...}

class Cat {
  bool likes(Food food) {...}
}

...

class MockCat extends Mock implements Cat {}

class FakeFood extends Fake implements Food {}

test('...', () {
  registerFallbackValue(FakeFood());
  
  final cat = MockCat();
  when(() => cat.likes(any<FakeFood>()).thenReturn(true));
});

확장 메소드를 제대로 스텁/확인할 수 없는 이유는 무엇인가요?

문서에서 상황을 잘 설명해주고 있어서 직접 복사했습니다!

"확장 메서드는 정적 메서드처럼 취급되기때문에 스텁/확인할 수 없습니다. 즉, 호출은 인스턴스를 신경쓰지 않고 확장메서드로 바로 이동합니다. 따라서 확장 메서드에 대한 호출을 스텁 및 검증하면 항상 실제 확장 메서드가 호출됩니다.

class MyClass {...}

extension on MyClass {
  void foo() {}
}

class MockMyClass extends Mock implements MyClass {}

"위의 시나리오에서 MockMyClass에서 foo를 호출하면 항상(스텁이 아닌) 실제 foo()가 호출됩니다. 결과적으로 항상 실제 확장자가 호출되므로 확장자인 메서드를 스텁/검증할 수 없습니다"

모의 테스트에 대한 이야기는 여기까지 입니다!

다음주에는 유닛 테스트에 대해 이야기하고 직접 손을 더럽히겠습니다.

profile
2021.05.03) Flutter, BlockChain, Sports, StartUp

0개의 댓글