목차

 

테스트 우선하기

많은 개발자들은 빨리 구현해야 한다는 압박에 코드를 충분히 테스트하지 않고 기능을 구현한다.

 

압박 → 스트레스 테스트 안함 

 

시간이 흐르면

  • 코드가 복잡해짐( 가독성 저하)
  • 소프트웨어 품질 저하
  • QA 를 거치면서 버그 발생 
  • 버그 수정으로인한 버그 발생

TDD 를 적용!!

 

이점

  • 회귀테스트로 사용 할 수 있다.
    • 코드를 수정하거나 추가할 때 앞서 작성한 테스트 코드를 사용하여 다른 기능에 문제가 없는비 바로 확인
    • 변경한 코드로 인해 소프트웨어가 비정상적으로 동작하는 것을 사전에 막아줌
  • 버그 수정도 쉬워진다.
    • 버그가 발생하는 상황에 대한 테스트 코드를 추가하고 이를 통과시키면된다.
    • 버그를 고치는 과정에서 발생할 수 있는 새로운 버그를 놓치지 않을까 걱정할 필요가 없어진다.

TDD 전파하기

TDD 가 익숙해지면 오는 효과

  • 결함 감소
  • 스트레스 감소
  • 빠른 피드백

먼저 TDD 에 익숙해 지기위해서 "개인 프로젝트" 를 통해 TDD 연마 필수

 

레거시 코드에 대한 테스트 추가 방법

  • 레거시 코드에 대한 테스트를 많이 해보자
    • 어떻게 테스트 코드를 만들어야 할지 감이 잡힌다.
  • 테스트 코드를 만들기 힘든 부분은 일부 코드를 리팩토링해서 구조 변경
    • 테스트 하고 싶은 일부 코드부분을 분리하여 별도 클래스로 구현 : 범위가 작을수록  수월
    • 분리하려 만든 코드를  테스트 코드로 작성 : 점진적으로 코드를 분리 및 테스트 작성

TDD와 개발 시간

전체 개발 시간을 줄이려면 코딩 시간뿐만 아니라 테스트 시간과 디버깅 시간을 줄여야 한다.

  • 테스트 시간을 줄이는 방법 = 테스트 자동화 ▶ TDD 를 활용한 테스트 자동화
  • 디버깅 시간을 줄이는 방법  = 빠른 버그 발견 ▶ TDD 를 활용하여 초기에 버그 발견
  • 코딩 시간을 줄이는 방법 = 리팩토링을 통한 코드 구조와 가독성 개성 ▶ TDD 를 하면서 리팩토링
    • 리팩토링은 코드의 구조와 가독성을 개선한다. 즉, 미래에 코드 추가나 수정을 쉽게 해준다. 이는 곧 미래의 코딩 시간을 줄여준다.

# 하나의 기능을 구현할 때 테스트를 작성하여 통과할 때까지 구현을 하면 개별 요소에 대한 개발 시간을 단축 할 수 있다. 또한 브라우저를 실행해서 기능을 확인하는 하고 수정하는 과정도 줄일 수 있다. 

목차

 

테스트 범위

 

기능 테스트와 E2E 테스트

Functional Testing ≒ End to end Testing

  • 사용자 입장에서 시스템이 제공하는 기능이 올바르게 동작하는지 확인
  • 사용자가 직접 사용하는 웹 브라우저나 모바일 앱 부터 시작해서 데이터베이스나 외부 서비스에 이르기까지 모든 구성 요소를 하나로 엮어서 진행
  • 끝(브라우저)에서 끝(DB)까지 모든 구성 요소를 논리적으로 완전한 하나의 기능으로다룬다.
  • 끝에서 끝까지 올바른지 검사하기 때문에 E2E(End to end) 테스트로도 볼 수 있다.

통합 테스트 (intergration Testing)

시스템의  각 구성 요소가 올바르게 연동되는지 확인

소프트웨어의 코드를 직접 테스트

  • 통합 테스트 : 서버의 회원 가입 코드를 직접 테스트
  • E2E 테스트 : 앱을 통하여 회원 가입 가능을 테스트

단위 테스트 (Unit Testing)

개별 코드나 컴포넌트가 기대한대로 동작하는지 확인 

 - 빠르게 자신이 만든 코드를 빠르게 확인하기 위해서 사용

 - 종속성이 있는 다른 클래스들에서 버그가 나는것을 방지하기위해 사용

한 클래스나 한 메서드와 같은 작은 범위를 테스트

테스트 범위 간 차이

유형 설치 및 준비 실행 속도 외부 시스템 연동 상황 준비와 결과 확인
기능 테스트 웹 서버 구동, 앱 설치 느림 (브라우저/앱 구동 및 상호작용 포함) 필요 어렵거나 불가능할 때가 있음
통합 테스트 DB, 캐시 서버 등 연동 대상 구성 느림 (DB 연결, 소켓 통신 등)  필요 어렵거나 불가능할 때가 있음
유닛 테스트 테스트 코드 만 필요 빠름 (서버나 DB 준비 없음) 대역으로 처리 상대적으로 쉬움

 

통합 테스트의 필요성

  • 각 구성 요소가 올바르게 연동되는 것을 확인
  • 자동화하기 좋은 수단이 통합 테스트 코드

테스트 범위에 따른 테스트 코드 개수와 시간

기능 테스트를 수행하려면 모든 환경(브라우저, DB, 등)이 갖추어져야 하기 때문에 자동화하거나 다양한 상황별로 테스트하기 가장 어렵다.

통합 테스트는  시스템 내부 구성요소에 대한 테스트 가능, 기능 테스트보다 상황을 유연하게 구성할 수 있다.

단위 테스트는 통합 테스트로도 만들기 힘든 상황을 구성가능 → 단위 테스트에서 가능한 다양한 상황을 테스트

 

통합 테스트/기능 테스트 에서는 주요한 상황에 초점을 맞추어서 테스트

테스트 범위

WireMock을 이용한 Rest 클라이언트 테스트

통합 테스트하기 어려운 대상이 외부 서버이다. WireMock을 사용하면 서버 API를 스텁으로 대체 가능하다.

WireMockServer는 HTTP 서버를 흉내 낸다.

  • 테스트 실행 전에 WireMockServer를 시작한다. 실제 HTTP 서버가 뜬다.
  • 테스트에서 WireMockServer의 동작을 기술한다.
  • HTTP 연동을 수행하는 테스트를 실행한다.
  • 테스트 실행 후에 WireMockServer 중지한다

WireMock은 JSON/XML 응답, HTTPS 지원, 단독 실행 등 다양한 기능을 제공하므로 외부 연동 코드를 테스트할 때 유용하게 사용할 수 있다

스프링 부트의 내장 서버를 이용한 API 기능 테스트 

내장 서버에 연결하는 RestTemplate인 TestRestTemplate 를 이용하면 스프링 부트의 내장 서버를 이용한 API 기능 테스트 가능

 

목차

테스트가 어려운 코드

❏ 하드 코딩된 경로

...
Path path = Paths.get("D:\\data\\pay\\cpoo1.csv");
..

   - 완전하게 일치하는 경로에 파일이 없으면 테스트 불가능, 맥OS 나 리눅스 를 사용하는 개발자 역시 테스트 불가능

 

테스트를 어렵게 하는 하드코딩 예 : 파일의 경로, IP 주소, 포트 번호

 의존 객체를 직접 생성

public class PaySync {
  //의존 대상을 직접 생성                 
  private PayInfoDao payInfoDao = new PayInfoDao();

  public void sync() throws IOException{
    ...
    payInfos.forEach(pi -> payInfoDao.insert(pi);
  }
}

 

이 코드를 테스트하려면 PayInfoDao가 올바르게 동작하는데 필요한 모든 환경을 구성해야 한다.

DB를 준비해야 하고 필요한 테이블도 만들어야 한다

 

테스트를 실행하면 데이터가 DB에 추가되므로 같은 테스트를 다시 실행하기 전에 기존에 들어간 데이터를 삭제해야 한다.

그렇지 않으면 중복 데이터로 인해 데이터 갑입에 실패하게 된다.

 정적 메서드 사용

클래스의 인스턴스가 아니라 클래스에 직접 연결되어 있기 때문에, 일반적인 의존성 주입이나 모킹이 어렵다.

 

테스트할 때 특정 메서드의 행동을 모의로 대체하거나 변경하고 싶다면,

    인스턴스 메서드의 경우 해당 객체를 모킹가능,

    정적 메서드의 경우 그럴 수 없어 실제 메서드를 호출

이로 인해 테스트가 복잡해지고, 테스트 환경과 실제 환경 사이의 격리가 어려워진다.

 

 가능하다면 정적 메서드 대신 인스턴스 메서드를 사용, 필요한 의존성을 생성자나 세터를 통해 주입하는 것이 좋다.

 실행 시점에 따라 달라지는 결과

현재 시간이나 날짜를 기준으로 사용자의 구독상태를 계산하는 경우

Random 을 이용해서 임의 값을 사용하는 코드

→ 시점에 따라 테스트 결과가 달라지면 믿을 수 없는 테스트 코드가 된다.

 역할이 섞여 있는 코드

여러 역할이 섞여 있는 코드는 특정 기능만 테스트하기가 쉽지 않다.

 그 외 테스트가 어려운 코드

  • 메서드 중간에 소켓 통신 코드가 포함되어 있다.
  • 콘솔에서 입력을 받거나 결과를 콘솔에 출력한다.
  • 테스트 대상이 사용하는 의존 대상 클래스나 메서드가 final 이다. 이 경우 대역으로 대체가 어려울 수 있다.
  • 테스트 대상의 소스를 소유하고 있지 않아 수정이 어렵다.

테스트 가능한 설계

앞에서 살펴본 테스트가 어려운 주된 이유는 의존하는 코드를 교체할 수 있는 수단이 없기 때문

 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기

생성자나 세터를 이용해서 경로를 전달 받기

메서드를 실행할 때 인자로 전달받기

 의존 대상을 주입 받기

생성자를 통해서 의존 대상을 주입하게 수정

 테스트하고 싶은 코드를 분리하기

역할이 섞여 있는 코드에서는 테스트하고 싶은 코드를 별도 기능으로 분리해서 테스트를 진행

 시간이나 임의 값 생성 기능 분리하기

테스트 대상이 사용하는 시간이나 임의 값을 제공하는 기능을 별도로 분리해서 대역으로 처리

 외부 라이브러리는 직접 사용하지 말고 감싸서 사용하기

외부 라이브러리와 연동하기 위한 객체를 따로 만들고, 테스트 대상을 분리한 타입을 사용하게 변경

 

변경전

public class LoginService {
    private String authKey = "somekey";
    private CustomerRepository customerRepo;

    public LoginService(CustomerRepository customerRepo) {
        this.customerRepo = customerRepo;
    }

    public LoginResult login(String id, String pw) {
        int resp = 0;
        boolean authorized = AuthUtil.authorize(authKey);
        if (authorized) {
            resp = AuthUtil.authenticate(id, pw);
        } else {
            resp = -1;
        }
        if (resp == -1) return LoginResult.badAuthKey();

        if (resp == 1) {
            Customer c = customerRepo.findOne(id);
            return LoginResult.authenticated(c);
        } else {
            return LoginResult.fail(resp);
        }
    }

}

 

 

변경 후 코드

public class AuthService {
    private String authKey = "somekey";

    public int authenticate(String id, String pw) {
        boolean authorized = AuthUtil.authorize(authKey);
        if (authorized) {
            return AuthUtil.authenticate(id, pw);
        } else {
            return -1;
        }
    }
}

public class LoginService {
    private AuthService authService = new AuthService();
    private CustomerRepository customerRepo;

    public LoginService(CustomerRepository customerRepo) {
        this.customerRepo = customerRepo;
    }

    public void setAuthService(AuthService authService) {
        this.authService = authService;
    }

    public LoginResult login(String id, String pw) {
        int resp = authService.authenticate(id, pw);
        if (resp == -1) return LoginResult.badAuthKey();

        if (resp == 1) {
            Customer c = customerRepo.findOne(id);
            return LoginResult.authenticated(c);
        } else {
            return LoginResult.fail(resp);
        }
    }
}

 

의존하는 대상이 final 클래스거나 의존 대상의 호출 메서드가 final인 경우에도 동일한 기법으로 테스트 가능

 

목차

 

대역 필요성

"자동이체 기능"을 테스트하기 위한 외부 서비스에서 제공되는 정보를 이용하여 구현 시 문제가 되는 상황들

  • 해당 업체에서 상황별로 테스트 할 수 있는 카드번호 받아야함
  • 외부서비스에서 실수로 유효한 카드번호 삭제
  • 발급 받은 유요한 카드번호의 유효기간 지남

외부 서비스에서 제공하는 HTTP URL을 이용하여 제공되는 정보 구현이 어려움

 

외부요인이 테스트에 관여하는 주요 예시

  • 테스트 대상에서 파일 시스템을 이용
  • 테스트 대상에서 DB로 부터 데이터를 조회하거나 데이터를 추가
  • 테스트 대상에서 외부 HTTP 서버와 통신

TDD 는 "테스트 작성 -> 통과시킬 만큼 구형 -> 리팩토링" 의 과정을 짧은 흐름으로 반복해야 한다.

하지만, 테스트 대상이 이런 외부 요인에 의존하면 테스트를 작성하고 실행하기 어려워지며 실행 결과를 예측할 수 없게 만든다.

 

→ 이럴 때 외부 요인을 대신하는 대역을 이용하여 테스트를 작성한다.

 

대역을 이용한 테스트

대역을 이용하여  "자동이체 기능" 을 테스트하는 코드 작성해보기

 

CardNumberValidation을 대신한 대역 클래스

public class StubCardNumberValidator extends CardNumberValidator {
    private String invalidNo;
    private String theftNo;

    public void setInvalidNo(String invalidNo) {
        this.invalidNo = invalidNo;
    }

    public void setTheftNo(String theftNo) {
        this.theftNo = theftNo;
    }

    @Override
    public CardValidity validate(String cardNumber) {
        if (invalidNo != null && invalidNo.equals(cardNumber)) {
            return CardValidity.INVALID;
        }
        if (theftNo != null && theftNo.equals(cardNumber)) {
            return CardValidity.THEFT;
        }
        return CardValidity.VALID;
    }
}

- StubCardNumberValidation 에서 실제 카드번호 검증 기능을 구현하지 않고, VALID, INVALID 를 리턴하는 단순한 구현으로 대체

 

테스트 코드에 적용

....
@BeforeEach
void setUp() {
    stubValidator = new StubCardNumberValidator();
    stubRepository = new StubAutoDebitInfoRepository();
    register = new AutoDebitRegister(stubValidator, stubRepository);
}

@Test
void invalidCard() {
    stubValidator.setInvalidNo("111122223333");

    AutoDebitReq req = new AutoDebitReq("user1", "111122223333");
    RegisterResult result = this.register.register(req);

    assertEquals(INVALID, result.getValidity());
}

 

이와 같이 대역을 이용하면 필요한 외부 서비스 api 를 흉내내서 테스트에 적용할 수도 있다.

또한, DB 없이 DB 역할을 하는 클래스를 간단한게 구현하여 적용할 수도 있다.

 

대역의 종류

대역 종류  설명
스텁(Stub) 구현을 단순한 것으로 대체한다. 테스트를 위해 필요한 값만 제공하면 된다.
가짜(Fake) 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. DB 대신에 메모리를 이용해서 구현하는 방식이 이에 해당한다.
스파이(Spy) 호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다.
모의(Mock) 기대한 대로 상호작용하는지 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모듸 객체는 스텁이자 스파이도 된다.

 

❏ 약한 암호 확인 기능에 스텁 사용 ➡︎ 테스트를 위해 필요한 값 (암호가 약하다고 응답)만 제공

  • 실제 동작을 구현할 필요 x → 단순하게 약한 암호인지만 알려주면 된다.(스텁)
    • 스텁으로 하여금 암호 확인 요청이 오면 암호가 약하다고 응답하라고 설정
    • 실제 테스트 하는 클래스에 스텁용 클래스를 주입하여 결과를 리턴

      

❏ 리포지토리를 가짜 구현으로 사용 ➡︎ 리포지토리의 기본 기능을 구현

  • 동일한 ID를 가진 회원이 존재할 경우 익셉셥을 발생하는 테스트 → 가짜로 회원을 추가
    • User 클래스를 생성 - 엔티티 구현
    • 가짜 DB 로 MemoryUserRepository 클래스 구현
    • UserRepository 를 구현해서 가짜 DB에 메서드 추가

         Note : 상수를 이용하여 테스트를 통과시킨 다음에 구현을 일반화할 방법이 떠오르지 않으면
                     예를 추가하면서 점진적으로 구현을 완성해 나가면 된다.

 

❏ 이메일 발송 여부를 확인하기 위해 스파이를 사용

 

회원 가입에 성공하면 회원 가입 안내 메일 발송 여부를 확인하는 방법

  • 회원 가입 후 EmailNotifier "메일 발송 기능" 을 실행할 때 "가입한 이메일 주소" 를 사용했는지 확인 

EmailNotifier 의 스파이 대역(class SpyEmailNotifier) 구현하여 이메일 발송여부를 확인

  • 이메일 발송 기능을 호출 → SpyEmailNotifier 에서 호출 됨를 저장하여 True 로 반환
  • 이메일 발송 기능을 호출 → SpyEmailNotifier 에서 가입시 사용된 이메일을 저장하여 해당 이메일을 반환 
assertTrue(spyEmailNotifier.isCalled());
assertEquals("email@email.com", spyEmailNotifier.getEmail());

 

모의 객체로 스텁과 스파이 대체

- 주요기능 : 대역 객체(스텁, 스파이) 가 기대하는대로 상호작용했는지 확인하는 것

모의 객체를 위한 도구를 이용하면, 모의 객체가 기대하는대로 상호작용했는지 검증이 가능하다.

 

상황과 결과 확인을 위한 협업 대상(의존) 도출과 대역 사용

외부 API 를 직접 연동하여 테스트 코드에서 상황을 구현하거나 결과를 확인하기 어렵다.

이렇게 제어하기 힘든 외부상황이 존재하면 아래의 방법으로 의존을 도출하고 이를 대역으로 대신 할 수 있다.

  • 제어하기 힘든 외부 상황을 별도 타입으로 분리
  • 테스트 코드는 별도로 분리한 타입의 대역을 생성
  • 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
  • 대역을 이용해서 상황 구성

"자동 이체 정보 등록 기능" 에서 외부 상황 분리

  • 카드번호가 유효한지 검사 하는 기능을 별도 타입으로 분리하고 대역을 생성
    • 별도 타입 : 카드번호 검사 기능 → 대역을 이용하여 구현 및 생성

회원 가입에 성공한 경우 이메일을 발송하는 기능

  • 회원 가입 기능 실행 이후에 이메일 발송 여부를 확인할 수단 필요
    • 이메일 발송 자체를 UserResiter 에서 구현하면 발송 여부를 확인하기 어려움
    • 결과 확인과 관련된 기능을 별도 타입으로 분리 → 대역으로 대체

당장 구현하기 어렵거나 오래걸리는 로직도 분리하기에 좋은 후보다.

대역과 개발 속도

TDD 과정에서 대역을 사용하지 않고 실제 구현을 사용한다면 발생하는 상황들

  • 카드 정보 제공 업체에서 도난 카드번호를 받을 때까지 테스트를 기다린다.
  • 카드 정보 제공 API가 비정상 응답을 주는 상황을 테스트하기 위해 업체의 변경 대응을 기다린다.
  • 회원 가입 테스트를 한 뒤에 편지가 도착할 때까지 메일함을 확인한다.
  • 약한 암호 검사 기능을 개발할 때까지 회원 가입 테스트를 대기한다.

모두 대기 시간이 발생 ➔ 대역을 사용하면 실제 구현이 없어도 실행 결과를 확인 가능

 

대역은 의존하는 대상을 구현하지 않아도 테스트 대상을 완성할 수 있게 만들어주며
이는 대기시간을 줄여주어 개발 속도를 올리는데 도움이 된다.

 

모의 객체를 과하게 사용하지 않기

 

모의 객체 사용의 편리성

  • 대역 클래스를 직접 만들 필요 없이 테스트 가능.

문제점

  • 결과 검증 코드가 길고 복잡해짐.
  • 여러 모의 객체 사용 시 코드 복잡도 증가.
  • 메서드 호출 검증 위주로 상호 작용이 조금만 바뀌어도 테스트가 깨질 위험.

주의사항

  • 모의 객체의 메서드 호출을 결과 검증의 주 수단으로 사용하지 말 것.

 

대안

  • DAO나 리포지토리 등 저장소에 대한 대역으로 메모리 기반의 가짜 구현 사용 권장.
  • 테스트 코드 관리가 용이해짐.

실용적 접근법

  • 모의 객체는 필요한 곳에 최소한으로 사용
  • 단위 테스트에서 모의 객체를 과도하게 사용하는 것보다, 통합 테스트를 통해 실제 상호작용을 검증하는 것이 좋음

 

 

 

 

 

목차

JUnit 5 모듈 구성

Assertions 클래스의 주요 단언 메서드

테스트 라이프사이클

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

추가 애노테이션: @DisplayName, @Disabled

모든 테스트 실행하기

 

JUnit 5 모듈 구성

JUnit 5는 크게 세 개의 요소로 구성되어 있다.

  • JUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공

  • JUnit 주피터(Jupiter) : JUnit 5를 위한 테스트 API와 실행 엔진을 제공

  • JUnit 빈티지(Vintage) : Junit 3과 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈을 제공

Assertions 클래스의 주요 단언 메서드

메서드 설명
assertEquals(expected, actual) actual 값이 expected 값과 같은지 검사한다.
assertNotEquals(unexpected, actual) actual 값이 unexpected 값과 같지 않은지 검사한다.
assertSame(Object expected, Object actual) 두 객체가 동일한 객체인지 검사한다.
assertNotSame(Object unexpected, Object actual) 두 객체가 동일하지 않은 객체인지 검사한다.
assertTrue(boolean condition) 값이 true인지 검사한다.
assertFalse(boolean condition) 값이 false인지 검사한다.
assertNull(Object actual) 값이 null 인지 검사한다.
assertNotNull(Object actual) 값이 null 이 아닌지 검사한다.
fail() 테스트를 실패 처리한다.

 

테스트 라이프사이클

■@BeforeEach 와 @AfeterEach

@BeforeEach 테스트를 실행하기 전  필요한 준비 작업을 할 때 사용
@AfeterEach 테스트를 실행한 후에 정리할 것이 있을때 사용 
  1. 테스트 메서드를 포함한 객체 생성
  2. (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
  3. @Test 애노테이션이 붙은 메서드 실행
  4. (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행

■@BeforeAll 와 @AfeterAll

@BeforeAll 한 클래스의 모든 테스트 메서드를 실행하기 전에 한번 실행
@AfeterAll 한 클래스의 모든 테스트 메서드를 실행한 후에 실행

 

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

테스트를 할때  테스트 메서드를 작성한 순서 대로 실행된다는 가정하에 테스트 메서드를 작성하면 안된다.

 

각 테스트 메서드는 서로 독립적으로 동작해야 한다. (서로 의존하면 안된다.)

  • 하나의 테스트의 결과가 다른 테스트에 영향을 미치면 안된다.
  • 테스트 메서드가 서로 필드를 공유하면 안된다.

 

추가 애노테이션: @DisplayName, @Disabled

java 는 메서드 이름만으로 테스트의 내용을 설명하기 부족할 수 있다.

이때 @DisplayName("테스트 내용") 을 이용해서 테스트 설명 가능

모든 테스트 실행하기

모든 테스트를 실행하는 경우

  • 코드를 원격 리포지토리에 푸시하기 전
  • 코드를 빌드해서 운영 환경에 배포하기 전

모든 테스트를 실행하여 실패하는 테스트가 존재하면 코드 푸시와 배포를 멈추고 원인을 찾기 위함.

 

 

실행하는 방법

  • 메이븐 : mvn test
  • 그레이들 : gradle test
  • 인텔리J, 이클립스: src/test/java 폴더에서 JUnit Test 실행

+ Recent posts