[Flutter] Automated Testing 1

yeahsilver·2023년 6월 22일
1
post-thumbnail

자동 테스트를 작성해야 하는 이유

  • 의도대로 앱이 동작하는지 보장 가능
  • 개발 시 버그를 잡을 수 있음
  • 어떤 기능을 변경할 시 regression 방지 가능
  • 자신감 있게 리팩터링 가능(?)
  • 처음부터 더 나은 코드를 작성할 수 있도록 도움을 줌
  • manual test에 비해 시간을 절약할 수 있음

테스트 타입

유닛테스트 (Unit Test)

: 개별 함수, 메소드, 클레스를 테스트 할 때 사용

Repository, data model, service, controller 각각을 별개로 테스팅할 때 유용함

정상적으로 작동한다면 빠르게 테스트를 진행할 수 있음

위젯 테스트 (Widget Test)

: 개별 위젯을 테스트할 때 사용

실기기 또는 애뮬레이터에서 실행하지 않기 때문에 빠르다는 특성을 가지고 있음

통합 테스트 (Integration Test)

: 앱 전체 또는 앱에서 비중을 많이 차지하는 부분을 테스트 할 때 사용

실기기 또는 시뮬레이터에서 실행되는 테스트이기에 테스트 시작 / 완료가 느림

client 측에서만 테스트 가능

E2E Test (End to End Test)

: 프론트엔드, 백엔드 모두 테스트 가능, 주로 시스템 자체가 의도대로 동작하는지 테스트할 때 사용

네트워크 연결 문제 등 외부 요소에 영향을 받는 경우가 있기에 세팅 및 유지보수가 어려움.

유닛 테스트 작성 방법

  1. 특정 Repository의 인스턴스를 정의

    final respotiroy = Repository();
  1. expect() 함수 추가

    expect(): 2개 이상의 인자를 가짐

    • first value: 체크하고 싶은 실제 값
    • second value: 기대하는 값
    expect(repository.get(), kTestValue);

TIP!

  • data mode에 toString() 메소드를 사용하면 instance of ___ 문구 대신 정확하게 어떤 값이 들어갔는지 확인 가능
  • equality operator를 사용하면 해시코드까지 비교하여 해당 인스턴스가 정확하게 같은 값인지 확인 가능

Exception을 활용한 테스트 방식

  1. test() 메소드 추가

  2. 아래와 같이 프로퍼티 추가

    test('get(1) returns first item', () {
        final repository = Repository();
        expect(
          productsRepository.get('1'),
          kTestArray[0],
        );
      });

만약 특정 데이터가 null인지 확인하고 싶으면, Property를 null로 변경 후 체크

test('get(1) returns first item', () {
    final repository = Repository();
    expect(
      productsRepository.get('1'),
      null,
    );
  });

exception을 처리하고 싶은데 null 데이터가 오는 경우에는 어떻게 처리해야하는가?

방법 1) Closure-based Syntax (throws error)

test('get(100) returns first item', () {
    final repository = Repository();
    expect(
      () => repository.get('100'), // here
	     noContentError, // repository.get('100')의 값이 NULL이라면, noContentError 호출
    );
  });
}

방법 2) No Closure (returns value)

Product? get(String id) {
    try {
      return _kTestArray.firstWhere((product) => product.id == id);
    } catch (e) {
      return null;
    }
  }

-------

test('get(100) returns first item', () {
    final repository = Repository();
    expect(
	     repository.get('100'), // here
	     null,
    );
  });
}

테스트 Grouping

group() 메소드를 사용하여 여러 테스트를 한꺼번에 실행할 수 있음

group('Repository', () {
	test(...);
	test(...);
})

Future / Stream 테스팅

Future

: Async / Await 사용

test("___", () async {
	final_
	expect(
		await _.fetchProductsList(),
		testData
	);
});

Stream

: emit() 와 같은 Stream Matcher 사용

test("___", () {
	final _;
	expect(
		_.fetchProductsList(),
		emits(testData); // emitting a value
	);
});

Advanced Stream Matcher 사용하기

  • emitsInOrder([]): stream에 추가되는 여러 값들을 체크

    예시

    test('currentUser is null after sign out', () async {
      final authRepository = makeAuthRepository();
      await authRepository.signInWithEmailAndPassword(testEmail, testPassword);
      expect(
          authRepository.authStateChanges(),
    			// emitsInOrder: to check the various values that are added to the stream
          emitsInOrder([
            testUser, // latest value from signInWithEmailAndPassword()
            null, // upcoming value from signOut()
          ]));
      await authRepository.signOut();
      expect(authRepository.currentUser, null);
    });

    예시 1과 동일한 작동을 하는 코드

    test('currentUser is null after sign out', () async {
      final authRepository = makeAuthRepository();
      addTearDown(authRepository.dispose);
      await authRepository.signInWithEmailAndPassword(
        testEmail,
        testPassword,
      );
      expect(authRepository.currentUser, testUser);
      expect(authRepository.authStateChanges(), emits(testUser));
    
      await authRepository.signOut(); // check one more tie after signing out
      expect(authRepository.currentUser, null);
      expect(authRepository.authStateChanges(), emits(null));
    });

Expect vs Verify

  • expect(): 아웃풋, state, 리턴값 체크
  • verify(): 액션 체크

예시

verify(authRepository.signOut).called(1);
expect(controller.debugState, const AsyncData<void>(null));

Type matchers

isA

: 리턴되는 값의 타입을 체크하는 메소드

예시

expect(controller.debugState, isA<AsyncError>());

Testing with Stream Matchers and Predicates

expectLater

  • expect와 동일한 기능을 하지만, Future을 리턴
    expectLater(
    	controller.stream,
    	emitsInOrder(const [
    		AsyncLoading<void>(),
    		AsyncData<void>(null),
    	])
    );

predicates

stack trace를 신경쓰지 않고 값 테스트 가능.

예시

expectLater(
	controller.stream,
	emitsInOrder(const [
		AsyncLoading<void>(),
		predicate<AsyncValue<void>>((value) {
			expect(value.hasError, true);
			return true;
		}),
	])
);

Testing Lifecycle

  • setUpAll(): 테스트가 실행되기 직전 단 한번만 호출되는 메소드
  • setUp(): 각 테스트가 실행되기 전 실행. 변수 초기화를 주로 진행
  • tearDownAll(): 테스트가 끝난 직후 단 한번만 호출되는 메소드

테스트가 끝났을 때 코드를 clear해야하는 경우

test('currentUser is not null after sign in', () async {
  final authRepository = makeAuthRepository();
  await authRepository.signInWithEmailAndPassword(
    testEmail,
    testPassword,
  );
  expect(authRepository.currentUser, testUser);
  expect(authRepository.authStateChanges(), emits(testUser));
  authRepository.dispose(); // cleanup
});

위와 같이 처리하게 되면 테스트가 실패했을 때 repository가 정상적으로 dispose되지 않음

addTearDown()사용

test('currentUser is not null after sign in', () async {
  final authRepository = makeAuthRepository();
  addTearDown(authRepository.dispose);
  await authRepository.signInWithEmailAndPassword(
    testEmail,
    testPassword,
  );
  expect(authRepository.currentUser, testUser);
  expect(authRepository.authStateChanges(), emits(testUser));
});

Acceptance Criteria

  • Given (어떻게 해당 액션이 시작되었는지)
  • When (어떤 행동을 취하는지)
  • Then (결과가 무엇인지)
group('submit', () {
  test('''
  Given) formType이 signIn 인 경우
  When) signInWithEmailAndPassword가 성공하면
  Then) true 반환 후, state를 Asyncs가 되어야함
  ''', () { ... });
});
expect(
  controller.debugState,
  // using predicate since we can't match the stack trace
  predicate<EmailPasswordSignInState>((state) {
    expect(state.formType, EmailPasswordSignInFormType.signIn);
    expect(state.value.hasError, true);
    return true;
  }),
);
profile
Hello yeahsilver's world!

0개의 댓글