Mvitimin / Microservices_study

Study for MSA
0 stars 0 forks source link

자바와 Junit을 활용한 실용주의 단위 테스트 #18

Open Mvitimin opened 6 months ago

Mvitimin commented 6 months ago

github https://github.com/gilbutITbook/006814

Mvitimin commented 6 months ago

1. 테스트 이름이 테스트 의도를 담을 수 있는 적절한 이름으로 지어야한다. matchAnswersFalseWhenMustMatchCriteriaNotMet matchAnswersTrueForAnyDontCareCriteria

public class ProfileTest {

   @Test
   public void matchAnswersFalseWhenMustMatchCriteriaNotMet() {
      Profile profile = new Profile("Bull Hockey, Inc.");
      Question question = new BooleanQuestion(1, "Got bonuses?");
      Answer profileAnswer = new Answer(question, Bool.FALSE);
      profile.add(profileAnswer);      
      Criteria criteria = new Criteria();
      Answer criteriaAnswer = new Answer(question, Bool.TRUE);
      Criterion criterion = new Criterion(criteriaAnswer, Weight.MustMatch);
      criteria.add(criterion);

      boolean matches = profile.matches(criteria);
      assertFalse(matches);
   }

   @Test
   public void matchAnswersTrueForAnyDontCareCriteria() {
      Profile profile = new Profile("Bull Hockey, Inc.");
      Question question = new BooleanQuestion(1, "Got milk?");
      Answer profileAnswer = new Answer(question, Bool.FALSE);
      profile.add(profileAnswer);      
      Criteria criteria = new Criteria();
      Answer criteriaAnswer = new Answer(question, Bool.TRUE);
      Criterion criterion = new Criterion(criteriaAnswer, Weight.DontCare);
      criteria.add(criterion);

      boolean matches = profile.matches(criteria);
      assertTrue(matches);
   }
}

2. @Before 메서드로 테스트 초기화

public class ProfileTest {
   private Profile profile;
   private BooleanQuestion question;
   private Criteria criteria;

   @Before
   public void create() {
      profile = new Profile("Bull Hockey, Inc.");
      question = new BooleanQuestion(1, "Got bonuses?");
      criteria = new Criteria();
   }

   @Test
   public void matchAnswersFalseWhenMustMatchCriteriaNotMet() {
      profile.add(new Answer(question, Bool.FALSE));      
      criteria.add(
            new Criterion(new Answer(question, Bool.TRUE), Weight.MustMatch));

      boolean matches = profile.matches(criteria);

      assertFalse(matches);
   }

   @Test
   public void matchAnswersTrueForAnyDontCareCriteria() {
      profile.add(new Answer(question, Bool.FALSE));      
      criteria.add(
            new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare));

      boolean matches = profile.matches(criteria);

      assertTrue(matches);
   }
}
Mvitimin commented 6 months ago

HamCrest 단언

1. AssertTrue

  @Test
   public void hasPositiveBalance() {
      account.deposit(50);
      assertTrue(account.hasPositiveBalance());
   }

2. assertThat은 명확한 값을 비교

 @Test
   public void assertDoublesCloseTo() {
      assertThat(2.32 * 3, closeTo(6.96, 0.0005));
   }

3.예외를 기대하는 방법 1) 단순한 방식: 어노테이션 사용

   @Test(expected=InsufficientFundsException.class)
   public void throwsWhenWithdrawingTooMuch() {
      account.withdraw(100);
   }

2) ExpectedException 규칙사용

 @Rule
   public ExpectedException thrown = ExpectedException.none();  

   @Test
   public void exceptionRule() {
      thrown.expect(InsufficientFundsException.class); 
      thrown.expectMessage("balance only 0");  

      account.withdraw(100);  
   }
Mvitimin commented 6 months ago

문서로서의 테스트

1.일관성있는 이름으로 테스트 문서화

멋지지 않은 이름 멋지고 설명적인 이름
makeSingleWithdrawal withdrawalReducesBalanceByWithdrawnAmount
attemptToWithdrawTooMuch withdrawalOfMoreThanAvailableFundsGeneratesError
multipleDeposits multipleDepositsIncreaseBalanceBySumOfDeposits

2.테스트를 의미 있게 만들기

1.지역변수 이름 개선하기 2.의미 있는 상수 도입하기 3.햄크래스트 단언 사용하기 4.커다란 테스트를 작게 나누너 집중적인 테스트 만들기 5.테스트 군더더기들을 도우미 메서드와 @Before 메서드로 이동하기

example

• @Before createAccount
• @Test depositIncreasesBalance
• @After closeConnections
• @Before createAccount
• @Test hasPositiveBalance
• @After closeConnections

3.BeforeClass 와 AfterClass 어노테이션

@BeforeClass initializeSomethingReallyExpensive
@Before createAccount
@Test depositIncreasesBalance
@After closeConnections
@Before createAccount
@Test hasPositiveBalance
@After closeConnections
@AfterClass cleanUpSomethingReallyExpensive
Mvitimin commented 6 months ago

좋은 테스트의 FIRST 속성

테스트 문제점 checklist

• 테스트를 사용하는 사람에게 어떤 정보도 주지 못하는 테스트
• 산발적으로 실패하는 테스트
• 어떤 가치도 증명하지 못하는 테스트
• 실행하는 데 오래 걸리는 테스트
• 코드를 충분히 커버하지 못하는 테스트
• 구현과 강하게 결합되어 있는 테스트. 따라서 작은 변화에도 다수의 테스트가 깨집니다.
• 수많은 설정 고리로 점프하는 난해한 테스트

1. FIRST: 좋은 테스트 조건

• [F]ast: 빠른 • [I]solated: 고립된 • [R]epeatable: 반복 가능한 • [S]elf-validating: 스스로 검증 가능한 • [T]imely: 적시의

2.[F]IRST: 빠르다

테스트 코드는 빠르게 동작하며, 느린 것에 의존하는 코드를 최소화한다면 작성하기도 쉬워집니다. 이러한 의존성을 최소화하는 것 역시 좋은 설계의 목표입니다. 다시 한 번 말하지만, 코드를 클린 객체 지향 설계 개념과 맞출수록 단위 테스트 작성도 쉬워집니다.

  public Map<String, Map<Boolean, AtomicInteger>> responsesByQuestion(
         List<BooleanAnswer> answers) {
      Map<Integer, Map<Boolean, AtomicInteger>> responses = new HashMap<>();
      answers.stream().forEach(answer -> incrementHistogram(responses, answer));
      return convertHistogramIdsToText(responses);
   }

answers, questions를 모두 입력받게 변경

   public Map<String, Map<Boolean, AtomicInteger>> responsesByQuestion(
         List<BooleanAnswer> answers, Map<Integer,String> questions) {
      Map<Integer, Map<Boolean, AtomicInteger>> responses = new HashMap<>();
      answers.stream().forEach(answer -> incrementHistogram(responses, answer));
      return convertHistogramIdsToText(responses, questions);
   }
public class StatCompilerTest {
   @Test
   public void responsesByQuestionAnswersCountsByQuestionText() {
      StatCompiler stats = new StatCompiler();
      List<BooleanAnswer> answers = new ArrayList<>();
      answers.add(new BooleanAnswer(1, true));
      answers.add(new BooleanAnswer(1, true));
      answers.add(new BooleanAnswer(1, true));
      answers.add(new BooleanAnswer(1, false));
      answers.add(new BooleanAnswer(2, true));
      answers.add(new BooleanAnswer(2, true));
      Map<Integer,String> questions = new HashMap<>();
      questions.put(1, "Tuition reimbursement?");
      questions.put(2, "Relocation package?");

      Map<String, Map<Boolean,AtomicInteger>> responses = 
            stats.responsesByQuestion(answers, questions);

      assertThat(responses.get("Tuition reimbursement?").
            get(Boolean.TRUE).get(), equalTo(3));
      assertThat(responses.get("Tuition reimbursement?").
            get(Boolean.FALSE).get(), equalTo(1));
      assertThat(responses.get("Relocation package?").
            get(Boolean.TRUE).get(), equalTo(2));
      assertThat(responses.get("Relocation package?").
            get(Boolean.FALSE).get(), equalTo(0));
   }
}

3.F[I]RST: 고립시킨다

테스트 코드는 어떤 순서나 시간에 관계없이 실행할 수 있어야 합니다.

각 테스트가 작은 양의 동작에만 집중하면 테스트 코드를 집중적이고 독립적으로 유지하기 쉬워집니다. 테스트에 두 번째 단언을 추가할 때 다음과 같이 스스로 질문해야 합니다. “이들 단언이 단일 동작을 검증하도록 돕는가, 아니면 내가 새로운 테스트 이름으로 기술할 수 있는 어떤 동작을 대표하는가?”

객체 지향 클래스 설계의 단일 책임 원칙(SRP)(‘SOLID 클래스의 설계 원칙’ 참고)에 따르면 클래스는 작고 단일한 목적을 가져야 합니다. 좀 더 구체적으로 SRP는 클래스를 변경해야 할 이유가 하나만 있어야 한다고 말합니다.

4.FI[R]ST: 좋은 테스트는 반복 가능해야한다.

반복 가능한 테스트는 실행할 때마다 결과가 같아야 합니다. 따라서 반복 가능한 테스트를 만들려면 직접 통제할 수 없는 외부 환경에 있는 항목들과 격리시켜야 합니다.

5.FIR[S]T: 스스로 검증 가능하다.

테스트는 기대하는 것이 무엇인지 단언하지 않으면 테스트가 아닙니다. 단위 테스트는 여러분 시간을 소모하기보다는 절약합니다. 테스트 결과를 수동으로 검증하는 것은 시간 소모적인 절차고 리스크가 늘어납니다. 멍해지기 쉽고 코드가 출력해 내는 거대한 로그를 보다가 중요한 신호를 놓칠 수도 있습니다.

테스트는 스스로 검증 가능할 뿐만 아니라 준비할 수도 있어야 합니다. 테스트를 실행하기 전에 수동으로 준비 단계를 만드는 어리석은 짓은 하지 마세요. 테스트에 필요한 어떤 설정 단계든 자동화를 해야 합니다. 기억하세요. 그럼에도 테스트를 실행하는 데 외부 설정이 필요하다면 FIRST 중에 I 부분(고립성)을 위반한 것입니다.

자가 검증성의 주제를 최대한 키워 봅니다. 테스트 코드는 전체 시스템을 위한 단위 테스트 묶음의 일부로 동작합니다. 때때로 수동으로 실행할 수도 있지만 한 단계 나아가 보면 테스트를 언제 어떻게 실행할지도 자동화할 수 있습니다.

6.FIRS[T]: 적시에 사용한다.

단위 테스트를 더 많이 할수록 테스트 대상 코드가 줄어듭니다. 그러면 먼저 단위 테스트 작성이 쉬워집니다. 또 두 번째로 새로운 코드를 넣었을 때 테스트 효과가 즉시 나타납니다.

마지막으로 옛날 코드에 대한 테스트는 시간 낭비가 될 수도 있습니다. 코드에 큰 결함이 없고 당장 변경할 예정이 없다면 (즉, 코드를 변경하지만 현재에서 어떤 것도 깨지 않아야 할 때) 여러분 노력은 거의 보상받지 못할 것입니다. 그 노력을 좀 더 말썽이 많고 역동적인 부분에 사용하세요.

Mvitimin commented 6 months ago

Right-BICEP: 무엇을 테스트할 것인가?

Right-BICEP은 무엇을 테스트할지에 대해 쉽게 선별하게 합니다.

• Right 결과가 올바른가? • B 경계 조건(boundary conditions)은 맞는가? • I 역 관계(inverse relationship)를 검사할 수 있는가? • C 다른 수단을 활용하여 교차 검사(cross-check)할 수 있는가? • E 오류 조건(error conditions)을 강제로 일어나게 할 수 있는가? • P 성능 조건(performance characteristics)은 기준에 부합하는가?

Right-[B]ICEP: 경계 조건은 맞는가?

  코드에 있는 분명한 행복 경로는 입력 값의 양극단을 다루는 코드 시나리오의 경계 조건에 걸리지 않을 수도 있습니다. 여러분이 마주치는 수많은 결함은 이러한 모서리 사례(corner case)이므로 테스트로 이것들을 처리해야 합니다.

생각해야 하는 경계 조건은 다음과 같습니다.

• 모호하고 일관성 없는 입력 값. 예를 들어 특수 문자("!*W:X\&Gi/w$→>$g/h#WQ@)가 포함된 파일 이름 • 잘못된 양식의 데이터. 예를 들어 최상위 도메인이 빠진 이메일 주소(fred@foobar.) • 수치적 오버플로를 일으키는 계산 • 비거나 빠진 값. 예를 들어 0, 0.0, "" 혹은 null • 이성적인 기댓값을 훨씬 벗어나는 값. 예를 들어 150세의 나이 • 교실의 당번표처럼 중복을 허용해서는 안 되는 목록에 중복 값이 있는 경우 • 정렬이 안 된 정렬 리스트 혹은 그 반대. 정렬 알고리즘에 이미 정렬된 입력 값을 넣는 경우나 정렬 알고리즘에 역순 데이터를 넣는 경우1 • 시간 순이 맞지 않는 경우. 예를 들어 HTTP 서버가 OPTIONS 메서드2의 결과를 POST 메서드보다 먼저 반환해야 하지만 그 후에 반환하는 경우

경계 조건에서는 CORRECT를 기억하라

• [C]onformance(준수): 값이 기대한 양식을 준수하고 있는가? • [O]rdering(순서): 값의 집합이 적절하게 정렬되거나 정렬되지 않았나? • [R]ange(범위): 이성적인 최솟값과 최댓값 안에 있는가? • [R]eference(참조): 코드 자체에서 통제할 수 없는 어떤 외부 참조를 포함하고 있는가? • [E]xistence(존재): 값이 존재하는가(널이 아니거나(non-null), 0이 아니거나(nonzero), 집합에 존재하는가 등)? • [C]ardinality(기수): 정확히 충분한 값들이 있는가? • [T]ime(절대적 혹은 상대적 시간): 모든 것이 순서대로 일어나는가? 정확한 시간에? 정시에?

Mvitimin commented 6 months ago

Mock 객체 사용

example


public class AddressRetriever {
   private Http http;

   public AddressRetriever(Http http) {
      this.http = http;
   }

   public Address retrieve(double latitude, double longitude)
         throws IOException, ParseException {
      String parms = String.format("lat=%.6flon=%.6f", latitude, longitude);
      String response = http.get(
         "http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"
         + parms);

      JSONObject obj = (JSONObject)new JSONParser().parse(response);
      // ...

      JSONObject address = (JSONObject)obj.get("address");
      String country = (String)address.get("country_code");
      if (!country.equals("us"))
         throw new UnsupportedOperationException(
               "cannot support non-US addresses at this time");

      String houseNumber = (String)address.get("house_number");
      String road = (String)address.get("road");
      String city = (String)address.get("city");
      String state = (String)address.get("state");
      String zip = (String)address.get("postcode");
      return new Address(houseNumber, road, city, state, zip);
   }
}

public interface Http {
   String get(String url) throws IOException;
}

public class HttpImpl implements Http {
   public String get(String url) throws IOException {
      CloseableHttpClient client = HttpClients.createDefault();
      HttpGet request = new HttpGet(url);
      CloseableHttpResponse response = client.execute(request);
      try {
         HttpEntity entity = response.getEntity();
         return EntityUtils.toString(entity);
      } finally {
         response.close();
      }
   }
}

번거로운 동작을 스텁으로 대체

public class AddressRetrieverTest {
   @Test
   public void answersAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {

// 람다를 활용하여 스텁 구현을 동적으로 생성합니다.
      Http http = (String url) -> 
         "{\"address\":{"
         + "\"house_number\":\"324\","
         + "\"road\":\"North Tejon Street\","
         + "\"city\":\"Colorado Springs\","
         + "\"state\":\"Colorado\","
         + "\"postcode\":\"80903\","
         + "\"country_code\":\"us\"}"
         + "}";
      AddressRetriever retriever = new AddressRetriever(http);

      Address address = retriever.retrieve(38.0,-104.0);

      assertThat(address.houseNumber, equalTo("324"));
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }

   @Test
   public void returnsAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      Http http = new Http() {
         @Override
         public String get(String url) throws IOException {
            return "{\"address\":{"
               + "\"house_number\":\"324\","
               + "\"road\":\"North Tejon Street\","
               // ...
               + "\"city\":\"Colorado Springs\","
               + "\"state\":\"Colorado\","
               + "\"postcode\":\"80903\","
               + "\"country_code\":\"us\"}"
               + "}";
            }};
      AddressRetriever retriever = new AddressRetriever(http);

      Address address = retriever.retrieve(38.0,-104.0);

      assertThat(address.houseNumber, equalTo("324"));
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
}

목 도구를 사용하여 테스트 단순화

public class AddressRetrieverTest {
   @Test
   public void answersAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      Http http = mock(Http.class);
      when(http.get(contains("lat=38.000000&lon=-104.000000"))).thenReturn(
            "{\"address\":{"
            + "\"house_number\":\"324\","
           // ...
            + "\"road\":\"North Tejon Street\","
            + "\"city\":\"Colorado Springs\","
            + "\"state\":\"Colorado\","
            + "\"postcode\":\"80903\","
            + "\"country_code\":\"us\"}"
            + "}");
      AddressRetriever retriever = new AddressRetriever(http);

      Address address = retriever.retrieve(38.0,-104.0);

      assertThat(address.houseNumber, equalTo("324"));
      // ...
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
}

InjectMock


public class AddressRetrieverTest {
   @Mock private Http http;
   @InjectMocks private AddressRetriever retriever;

   @Before
   public void createRetriever() {
      retriever = new AddressRetriever();
      MockitoAnnotations.initMocks(this);
   }

   @Test
   public void answersAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      when(http.get(contains("lat=38.000000&lon=-104.000000")))
         .thenReturn("{\"address\":{"
                        + "\"house_number\":\"324\","
         // ...
                        + "\"road\":\"North Tejon Street\","
                        + "\"city\":\"Colorado Springs\","
                        + "\"state\":\"Colorado\","
                        + "\"postcode\":\"80903\","
                        + "\"country_code\":\"us\"}"
                        + "}");

      Address address = retriever.retrieve(38.0,-104.0);

      assertThat(address.houseNumber, equalTo("324"));
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
}