1. 목적

숨어있는 결함(fault)을 찾아내기 위해 소프트웨어를 실행하는 행위와 절차


2. 원칙

  • 마이어 법칙 : 개발자에 의한 테스트는 지양한다.
  • 40:20:40 -> 설계 40, 개발 20, 테스트 40 비율
  • 테스트 품질지표를 선정하고 초기단계부터 계획하고 실행

3. 전략

전담테스트 조직을 운영함으로서 전문성과 효율성을 높인다.

사용자를 참여시켜 테스트의 신뢰성을 높인다.

품질관리(통제, 감사)와 연계한다.


4. 기능적 테스트 / 비기능적 테스트

4.1. 기능적 테스트란?

  • 소프트웨어가 수행하는 "어떤" 기능을 테스트
  • 문서화되어 있거나 테스터가 알고 있는 기능 특징, 그것들간의 상호운용성을 평가

4.2. 기능적 테스트의 특징

  • 명세 기반 기법 : 테스트 조건과 테스트 케이스 도출에 이용
  • 소프트웨어의 외부적인 행동을 고려 : 블랙 박스 테스트
  • ISC/IEC 9126의 품질 주특성인 기능성에 대한 테스트 (보안성 테스트, 상호운용성 테스트 포함)

5. 테스트 피라미드

2009년 마틴 파울러가 제시한 테스트 방법(TestPyramid (martinfowler.com))으로 테스트를 크게 세가지 범주로 구분한다.

  1. Unit Test 
  2. Integration Test 
  3. UI Test 

1이 피라미드 하부, 3이 피라미드 상부이다. 상부로 올라갈 수록 테스트 수는 줄어든다. 이 말은 단위 테스트는 많이 진행된다는 것이다.

  • 피라미드 하부는 적용 가장 쉽고 비용 가장 적게 든다. 그리고 빠르고 간단한 테스트(단위 테스트)가 진행된다.
  • 피라이브 상부는 적용 가장 어렵고 비용 가장 많이 든다. 그리고 느리고 복잡하고 종단 간의 테스트가 진행된다.

5.1. Unit Test

가장 쉽고 빠르게 적용할 수 있다. 주로 단일 함수나 메소드 단위로 애플리케이션 로직을 테스트한다. 핵심적인 목표는 기능 정상 동작 여부이다. <테스트 커버리지>라는 지표를 통해 전체 코드 대비 얼만큼 테스트되었는지 측정할 수 있다.

5.2. Integration Test

화면과 같은 사용자 인터페이스를 경유하여 테스트하는 것으로 서비스 테스트라고도 하며, 코드와 시스템이 어떻게 상호작용하는지 확인한다. 모놀리식 애플리케이션과 마이크로서비스 간에 약간 차이가 있는데, 모놀리식은 화면에 서비스를 제공하는 대상만 테스트할 수 있지만 마이크로서비스의 경우는 관련된 개별 서비스까지 테스트할 수 있게 된다.

이 테스트는 실패할 경우 어디에서 문제가 발생하였는지 찾기가 어렵긴 하다. 

5.3. UI Test

E2E 레벨의 전반적인 UI/UX 테스트이다. 만약 테스트를 통과하면 강한 확신을 얻을 수 있다.


6. 테스트 자동화

테스트 자동화를 위한 장애물이 많다.

  • 도구가 너무 많고, 상용 도구 투자 비용, 테스트를 위한 스크립트 유지보수, 테스트 자동화 코드 작성 등
  • 초기에는 분명 수동 테스트를 진행할 때보다 더 많은 리소스(인력)을 필요로 한다!

6.1. 테스트 자동화 범위

  • Static Analysis (정적분석) : 코딩 품질 향상 및 Rule 체크, SonarQube/Spotless 등 사용
  • Unit Test : 클래스 및 메소드의 "단위 테스트"로 특정 모듈이 의도한대로 작동하는지 검증하는 것이다. Unit/Mocha/Chai 등 사용, U/I은 매뉴얼 수행 고려
  • Module (API) Test : Cypress/Postman 등 사용, API Gateway에 등록되는 API는 모두 수행 고려
  • Health Check : Cypress/Postman 등 사용, 서비스/컴포넌트 전체
  • Smoke Test : Cypress/Postman 등 사용, 중요 업무 대상 

6.2. 장점

  • 반복 업무 사라짐
  • 일관성과 반복성에 따른 가치 제공
  • 사람이 불가능한 작업 수행 가능 (부하 테스트)
  • 자동화를 하지 않으면 회귀 테스트에 많은 공수가 들고 결국 회귀 테스트가 불가능해지고 CI/CD가 불가능

6.3. 리스크

  • 모든 것을 자동화할 수 없다. (UI 등) 감각, 지식, 경험 등을 기반으로 한 사람과 같을 수 없단 말이다. 물론 최대한 사람 기반의 패턴을 반영하는 것이 중요하다. 
  • UI 의존성이 큰 영역, 사용자 요구사항이 너무 빈번하게 변경되는 영역, 임시적인 기능은 자동화 제외 검토
  • 자동화를 위한 추가 코드 개발 및 유지보수에 필요한 노력을 투여해야 한다.
  • 너무 자동화에만 집중하여 정작 결함을 찾아내지 못할 수 있다.

6.4. API 테스트 자동화

6.4.1. 수동 테스트

자동화가 되지 않은 API 테스트 시에는, 도구를 이용한다고 해도 결국 사람이 결과값을 확인해야 한다. 또 매뉴얼하게 수행함에 따라 테스트 이력관리의 어려움이 있고 결함이 발생한 정확한 시점을 알기 어렵다. 

수동 테스트는 다음과 같은 방법이 있다.

  • API 개발 후에 화면에서 호출하여 테스트 : API와 화면이 모두 개발되어야 테스트 가능하므로 시점이 너무 늦게 된다. 따라서 단위 테스트에는 적합치 않다.
  • API 개발자 도구를 통해 API 호출 : (Chrome Plugin, HTTP4e) 편리함이 있지만 자동화까지의 기능은 없다. 또 결과값은 수동으로 확인해야 한다.

6.4.2. 자동 테스트

자동화 방식은 다음과 같다.

  • 자동화 스크립트 자체는 형상관리(git)
  • 스크립트에는 테스트 데이터 포함. 따라서 해당 API를 호출해야 하는 다른 API 개발자도 활용 가능

6.4.3. 업무 선정

전체 API를 자동화할 수는 없다. 현실적으로 많은 비용이 투여되고, 이미 운영 중인 API에 대해 테스트 자동화하기는 어렵기 때문이다. 따라서 목적에 맞는 대상을 잘 선별하여 자동화하는 것이 중요하다. 

6.4.4. 관련 도구

  • SOAP UI : 오픈 소스와 상용 버전이 있음. 
  • Postman : 일반 버전과 Subscription 형태의 유료 버전이 있음.
  • Katalon : 상용 없이 오픈 버전만 제공. Object Repository 개념 제공.

7. 스모크 테스트

예비 테스트라고도 한다. 새로 추가된 기능이 기존 핵심 기능에 영향을 미치는지 쭉 돌려보는 테스트다.

이름의 유래는 하드웨어 전원을 켰을 때 연기가 나지 않으면 테스트 통과, 라는 것에서 유래한다.

스모크 테스트는 다음 조건을 필요로 한다.

  • 모든 구성원이 스모크 테스트를 할 수 있다.
  • 테스트 소요 시간은 10분 이내
  • 테스트는 사람들이 생각하는 핵심 기능들을 고루 검증할 수 있어야 함

8. 유닛 테스트

함수와 메소드에 대한 테스트를 케이스를 작성하여 특정 모듈이 의도한대로 동작하는지 검증하는 테스트이다. 

유닛 자체의 불확실성을 제거하므로 상향식 테스트에 적합하다. 

코드 커버리지는 테스트가 얼마나 충분한지 나타내는 지표이다. 예를 들어 100줄의 코드 중 80줄의 테스트가 진행되면 커버리지는 80%다. 코드 커버리지의 기준은 다음과 같다.

  • 구문 (Statement) : 코드 라인이 한번 이상 실행되면 충족
  • 조건 (Condition) : 각 내부 조건이 참/거짓을 가지게 되면 충족
  • 결정 (Decision) : 전체적인 결과가 참/거짓이면 충족

테스트에 소요되는 시간이 없지 않지만 그래도 얻는 장점이 많고 궁극적으로는 생산성을 올려준다고 할 수 있다. 장점은 다음과 같다.

  • 테스트 케이스가 잘 작성되어 있다면 개발 과정 중 미리 문제를 파악할 수 있다.
  • 코드 변경에 따른 영향도를 쉽게 파악할 수 있다.
  • 리팩토링이 용이하다. (테스트 케이스는 일종의 보험이다)
  • 페어 프로그래밍을 수행한다면 번갈아가면서 테스트 케이스를 작성할 수 있다.

기존 코드가 있을 때 모두 다 테스트 코드를 작성해야 할까? 가성비를 따져야겠지만, 기존 코드는 E2E 테스트로 커버하고 가능하면 새로 개발하는 부분에 대해 테스트를 시작하는 것이 좋다.

일반적인 작성 방법은 다음과 같다.

  • 하나의 테스트 케이스에 최소한의 기능을 검증하고 최대한 간결하게 작성한다.
  • 입력 값에 대한 결과 값을 검증하는 방식으로 작성한다.
  • 커버리지 수치를 높인다. 하지만 커버리지 수치 자체가 목적이 되어서는 안된다. 의미없는 테스트는 하지 말자.
  • 서드 파티 라이브러리는 일단 믿고 테스트 검증 대상에서 제외한다.

9. MSA 테스트

Bounded Context에서 마이크로서비스가 생겨나서 제공자와 소비자간의 합의를 통해 스펙이 결정되며, REST/HTTP 기반 혹은 PUB/SUB 기반 통신을 하고 있다.

모놀리식 아키텍처에 비해 MSA는 민첩성을 얻을 수 있지만 코드 변경은 여전히 여러 위험을 초래한다.

게다가 서비스가 점점 작아지고/많아지고, API 개선이 잦아지고, 글로벌리 팀간의 협업이 요구되기도 한다. 게다가..

  • 소비자에 영향없이 제공자 서비스에 새로운 기능을 추가하거나 혹은 삭제하고 싶다.
  • 제공자는 소비자가 서비스를 사용하고 있는지 알 수 있는가?
  • 제공자는 소비자가 소비자가 원하는 서비스를 제공하고 있는지 알 수 있는가?
  • 자주 배포할 수 있는가?

MSA에서 테스트를 제대로 하려면 런타임에 모든 앱간의 의존관계가 확립되어야 하고, 통합 환경에서 전 구간의 테스트를 수행해야 한다. 그러나 방대한 구간의 환경을 수행하는데 정말 많은 시간과 비용이 필요하다.

일단 클라우드 네이티브한 배포 전략은 다음과 같다.

  • Blue/green 배포
  • Canary 배포
  • A/B 배포
  • Traffic Shadowing

각 배포의 자세한 설명은 이미 너무 많은 자료들이 있어 생략한다.

9.1. CDC (소비자 주도 계약, Consumer Driven Contract)

소비자 주도 계약은 소비자의 요구사항 중심으로 제공자 서비스를 진화히시키 위한 협업 패턴으로 E2E 테스트에 투여되는 비용을 줄일 수 있다. 대체적인 장점은 다음과 같다.

  • 제공자는 불필요한 서비스 개발을 줄일 수 있다.
  • 제공자는 개선에 대한 통찰을 얻을 수 있다.
  • Agile 실현을 위한 자동화를 가속한다. 

소비자(Consumer) 관점의 테스트로 서비스에 대한 소비자의 기대 사항을 정의한다. 이 기대 사항은 테스트될 수 있도록 코드로 표현한다. 테스트는 생산자가 수행한다. 특성은 다음과 같다.

  • Contract에는 호출 인터페이스가 기술되며, 런타임에서 필요한 마이크로서비스 간의 라이브러리는 공유하지 않는다.
  • 생산자가 먼저 Contract를 사용하는 통합 테스트를 정의하면서 stub을 공개한다.
  • CDC의 테스트는 생산자 API의 구체적인 내용은 감춘다.

이 계약이 완벽하게 수행되기 위해서는 생산자의 CI/CD의 일부로 포함되어야 한다.

9.2. Contract 테스트

API의 제공자와 사용자간의 규약에 대한 테스트이다.

API Spec이 바뀌면 다수의 소비자가 영향을 받으므로 제공자는 Contract 테스트가 필요하다. 문제가 발견되면 다행이나 물론 시간적 손실은 피할 수 없다.

배포 전에 Contract 테스트가 이루어져야 한다. 그리고 문제가 있다면 배포되지 않아야 한다. 만약 문제가 있는데 배포가 되면 UI 상에서 기능이 동작하지 않게 된다.

이렇듯 Contract 정합성 유지와 스펙과 일치하는 테스트 코드가 필요한데, 이를 해결하는 프레임워크들이 있다.

이를 위한 도구는 Pact와 Spring Cloud Contract이다.

  • Pact
    1) 소비자가 Contract를 Stub 코드를 작성하는 것처럼 자기 코드에서 작성하면
    2) Pact가 Pact file이라는 Contact 명세를 만들고 Pact Broker라는 저장소에 올린다.
    3) 그러면 소비자가 Pact Broker에서 Pact file을 꺼내 서비스가 Contact를 만족하는지 테스트한다.
     
  • Spring Cloud Contract
    1) 소비자는 생산자와 협의하여 필요한 Contract을 정의한다.
    2) 소비자는 Contract DSL을 작성한다. (언어는 그루비, YAML) 
    3) 소비자는 작성된 DSL을 공용 Repo에 업로드한다.
    4) 생산자는 소비자가 작성한 DSL을 다운로드한다.
    5) 생산자는 Spring Cloud Contract Verifier를 이용하여 테스트를 수행한다.
    6) 테스트가 정상 완료되면 생산자를 mocking한 stub.jar가 추출된다.
    7) 생산자는 Repo에 stub.jar를 업로드한다.
    8) 소비자는 stub.jar를 가져와서 실제 서비스라 가정하고 Stub Runner를 통해 명세에 대한 테스트를 수행한다.

Spring Cloud Contract는 소비자가 제공자 코드에 그루비 코드를 작성해야 한다는 문제점이 있다. 반면 Pact를 사용하면 제공자 코드를 건드릴 일은 없다.

Spring Cloud Contract의 주요 기능은 다음과 같다.

  • REST/HTTP, PUB/SUB 기반 통신을 모두 지원
  • 그루비나 YAML 기반 DSL을 제공
  • Contract 기반으로 자동으로 mock 테스트 수행 가능
  • 제공자가 Contract 기반으로 정의한 테스트 mock을 제공하고, 소비자가 테스트에서 사용 가능
  • Spring Cloud 연동 가능 (Eureka에 stub 자동 등록 지원)

Spring Cloud Contract의 DSL 공유 방안 예는 다음과 같다.

  1. 소비자는 생산자와 협의된 Contract을 생성하고 Git Repo에 Push한다.
  2. Git은 변경을 인지하고 빌드를 수행하며 Nexus에 업로드한다.
  3. 생산자는 빌드 수행시마다 Nexus에서 최신 DSL을 다운받아 해당 파일을 통해 개발/테스트한다.
  4. 테스트가 정장 수행되면 stub.jar 파일이 생성되고 Nexus에 엄로드된다.
  5. 소비자는 Contract 수행시 최신 버전의 stub.jar 파일 다운하여 mocking 하고 테스트 진행한다.

차이점을 정리하면 다음과 같다.

  Pact Spring Cloud Contract
환경   스프링 프레임워크
언어 루비, 자바, 자바스크립트, C# 자바
Contact 명세 위치 서비스 소비자 서비스 제공자

 

소스 안의 API 호출 지점과 Swagger에서 확인된 API Endpint 정보를 기반으로 서비스간 API 호출관계를 파악할 수 있다.

9.3. Documentation Driven Contract

요청자가 Contract를 Document 형태로 제공하여 개발하도록 하는 것

9.4. Cross Functional 테스트

비기능 테스트 및 속성 테스트

9.5. Component 테스트

마이크로서비스 내 모든 객체, 메소드에 대한 단위 테스트를 완료한 후 외부와 격리된 상태에서 전체를 테스트한다. 이렇게 격리된 상태에서 테스트를 수행하기 위해 타 마이크로서비스 호출에 대한 mock 및 stub이 필요하다.


10. E2E 테스트

E2E 테스트는 사용자 입장의 Workflow와 Latency를 측정하고 점검할 수 있다. 일반적으로 테스트는 프론트엔드, 백엔드, E2E 모든 영역에서 진행해야 하는 것이 맞지만 만약 여러가지 이유로 하나만 해야 한다면 E2E만 하는 것을 추천한다. 관련 테스트 도구로는 Selenium, Cypress, TestCafe 등이 있다.


11. UI 테스트 자동화

11.1. 도구 종류

  1. Selenium
    - 가장 유명한 도구라고 할 수 있다.
    - WebDriver라고 하는 웹 자동화 도구와 통합하는 작업이 진행되었다.
    - Script 작성 -> Selenium 라이브러리 -> Selenium 드라이버
     
  2. Cypress
    - Selenium과 비교하여 현재 개발 사례와 밀접하게 연계되어 있다.
     
  3. Appium
    - 모바일 쪽에서 많이 사용된다.
    - 1) UIAutomator2 :  안드로이드 GUI 객체 조작
    - 2) FacebookWDA : iOS의 GUI 객체 조작
     
  4.  SikuliX
    - Windows, Linux/Unix, Mac에 화면에 표시된 모든 것을 자동화한다.
    - 카드 결제 자동화 시에 적용 가능하는 경우가 있다.
     
  5. Katalon

11.2. 고민사항

CI/CD 파이프라인에서 모듈이 컴파일되고, 단위 테스트가 실행되고 통과하면 애플리케이션이 패키징된다. 그리고 이제 통합 테스트가 실행된다. 

테스트가 실패하면 다양한 사람에게 메일이 가거나.. 할 것이다. 혹은 대시보드에서 확인하게 될 것이다.

문제는 개발자 환경에서는 잘 되지만 CI/CD 파이프라인 상에서 실패하는 것이다.

11.3. Selenium & CI/CD

  • 우선 Selenium을 통해 테스트 스크립트를 작성한다.
  • GitLab을 설정한다. 
  • 테스트를 실행한다. 1) 코드가 푸시될 때마다 테스트 수행 2) Jenkins Cron을 통해 정기적으로 실행
  • 슬랙으로 알림을 받거나, 보고서를 받거나 한다.

12. 자바스크립트 테스트

12.1. QUnit

  • https://qunitjs.com/
  • 자바스크립트의 유닛 테스트 도구
  • 자바스크립트뿐 아니라 JQuery, JQuery UI, JQuery Mobile 등 사용 가능
  • JQuery의 일부였으나 점점 확장되다가 독립함

12.2. Jasmin

  • TDD가 아닌 BDD(Behavior-Driven) 지원

12.3. Mocha.js

  • TDD, BDD를 모두 지원하는 도구

13. Mock & Stub

테스트 환경 구축을 위해 많은 시간이 필요할 때 mock 객체를 사용한다. 예를 들어 DB서버, 웹서버, FTP 서버 등.. 

또 테스트를 위해서 특정 모듈이 필요한데 이에 대한 추가적인 협의, 정책이 필요한 경우 mock 객체로 커버한다.

이렇듯 전체를 구성하지 않고 흉내를 내는 mock을 활용한 테스트는 가성비가 좋다. 하지만 테스트가 통과한다고 하더라도 실제 서비스에서는 실패할 가능성이 있고 소비자가 요구하는 스펙과 일관성을 유지하기 어려운 것은 여전한 문제이다. 테스트는 임의의 케이스를 만들어서 성공하게 되는데, 이 케이스의 정합성이 맞는지 보장할 수 없고 스펙으로 정의한 데이터 파일의 지속적인 관리가 어렵다.

아래에 mock과 stub에 대한 차이점을 설명할테지만 간략한 차이점은 다음과 같다.

유형 장점 단점
Mock
  • 테스트 객체를 효율적으로 생성할 수 있음
  • 올바르게 호출되었는지 확인 가능
  • 단위 테스트에서 결과를 명확하게 제공
  • 어려움
Stub
  • 만들기 쉬움
  • 유연성에 제약 있음
  • 단위 테스트에서 결과를 명확하게 제공하지 않음
  • 올바르게 호출되었는지 확인하는 기능 없음

13.1. Mock

테스트를 위한 다른 위장 객체들과 다르게 행위검증 사용을 추구한다. 즉 행위를 기록하는 식의 로직이 들어가 있다.

"행위를 테스트한다 - test the behavior of some other object"

이러한 mock을 위한 프레임워크가 존재하는데 다음과 같다. 

  1. EasyMock
    - 오픈 소스이며 오래된 mock 프레임워크이다.
    - CreateMock(mock 객체 생성), Record(mock 객체의 예상되는 동작 지정), Replay(테스트 메소드 내에서 mock 객체 사용) 단계를 거친다.
     
  2. jMock
    - call-chain(연쇄 호출)이라는 특징이 있다. 이는 동일 객체에 여러 메시지를 보낼 수 있는 것이다.
     
  3. Mockito
    - 요즘 대세이다.
    - 작성이 쉽고 리팩토링이 쉽다. 
    - CreateMock(mock 객체 생성), Stub(mock 객체의 동작 지정), Exercise(테스트 메소드 내에서 mock 객체 사용), Verify(메소드가 예상대로 호출되었는지 검증) 단계를 거친다. 

이러한 mock 프레임워크를 사용하면 mock 클래스 관리의 부담을 덜고, 시간도 절약할 수 있다. Mockito를 사용한다면 별도의 클래스를 만들지 않고 Mock 객체를 만들어 테스트할 수 있으며, Verify 단계를 통해 호출 여부를 검증할 수 있다.

그런데 mock 프레임워크를 사용할 때는 여러 고민이 필요하다.

  • 정말 필요한 것일까?
  • mock을 사용하면 테스트 케이스 유지 관리가 복잡하고 어렵기 때문에 mock이 없는 구조로 만드는게 낫다.

13.2. Stub

mock이 행위를 검증한다면, stub은 상태를 검증한다.  

  • 호출이 되면 미리 준비된 답변으로 응답한다.
  • 프로그램된 것 외에는 응답하지 않는다. 따라서 로직에 따른 값의 변경은 없다.
  • 일반적으로 mock과 헷갈리는 경우가 있다.

예를 들면 환율 정보 제공 API일 때, 미리 정해진 환율 정보를 리턴한다.

앞서 stub은 상태를 검증한다고 했는데 이러한 환율 정보가 상태이고 이것을 검증(assertEquals 등)하는 것이다.

또다른 예로는 Order 서비스를 예로 들 수 있는데, 외부 Payment와 연동한다고 할 때 실제 Payment를 대체하여 만들어진 응답 결과를 반환할 것이다.