본문 바로가기
Programming/Java, Spring

[Java 기초문법] Testing code | junit을 이용한 테스팅, Edge case, Annuity 계산해보기, TDD로 guessing game implementing

by Renechoi 2022. 10. 19.

[Java 기초문법] by Professional Java Developer Career Starter: Java Foundations @ Udemy

 

 


 

JUnit 셋업하기 

 

 

프로젝트 스트럭처에서 Library>+버튼>From Maven

 

메이븐을 선택해준다. 

 

 

메이븐은 one of which it standardizes how we can organize our Java project. 

Which can also build our projects.

 

서로 다른 환경(IDE 등)에서 개발된 자바 프로젝트를 Standardize 해주는 툴

 

And also, MAVEN project has what are called repositories, where they've gathered up thousands of third party libraries and centralized them on these repositories. 

 

 

Here seleting Maven repositories, means we want to get the library from the standard maven repository.

 

 

org.junit.jupiter:junit-jupiter:

 

 

External Library에 보면 junit.jupiter가 생겼다. 

 

 

 

 

 

폴더 지정해주기

 

main folder를 선택하고 Sources 선택 => 메인 폴더로 지정 

 

 

test 폴더를 선택하고 Test 선택 => 테스트 폴더로 지정 

 

 

 

 

 

 

테스트 폴더에 junit 패키지와 클래스 만들기 

 

 

 

 

 

 


 

간단한 계산 기능을 테스트해보기 

 

 

테스트 폴더의 목적에 대해 생각해보자. 

 

what are some of the simplest things tha a calculator can do ? 

 

테스트 하려는 일의 simple한 시나리오를 가정해본다. 

 

" 0 + 0 " 

 

very most simple and obvious 

 

 

아래와 같이 인스턴스를 만들었을 때 아직 존재하지 않기 때문에 에러가 발생 

 

 

 

Option + return => 해결하는 옵션

 

이때 메인 폴더에 class를 생성할 수 있음 

 

 

 

 

 

 

 

새로운 클래스가 main 폴더에 생기면서 패키지와 클래스를 만들었다. 

 

 

Test로 돌아와서 sum 변수를 만들고 add에 따른 return을 받는다고 해보자. 

 

 

 

메인 폴더에 다시 add 함수를 만들어 해결하고 다시 Test로 돌아오기 

 

 

 

 

TEST 폴더로 돌아올 때 단축키 Ctrl + Shirt + T 

 

Assertion을 부여해보자. 

 

That I assert 0 + 0 = 0 

 

 

 

junit에서 불러올 것이기 때문에 import를 눌러 가져오는 옵션으로 선택 

 

 

 

 

package com.neutrio.calculator;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    public void canAddZeroPlusZero(){
        Calculator calc = new Calculator();
        int sum = calc.add(0,0);
        assertEquals()

    }
}

 

Here see the import static => very speific method is imported 

=> 

Assertions.assertEquals() 같은 방식으로 method 주소를 다 기입할 수도 있지만 

specific 하게 가져옴으로써 more shortcut ways 

 

 

 

=> meaning => I expect that sum eqauls 0 

 

method를 선택하고 command + p => shows options to be choosed 

 

 

 

애노테이션 달아주기 

 

annoation is a little snippet of code  => which serves as like metadata or a hint, and in this case, method. 

 

 

 

when we start run this class => junit will start up and will scan this code looking for a method annotated Test 

 

따라서 without annotation, class won't be run as a test.

 

 

test를 달면 Green play button이 옆에 생기고 누르면 

 

 

 

=> Test success

 

 

 

 


 

테스트 기능을 심화해보기 

 

 

이번에는 1+1 method를 만들고 test를 돌려보자. 

 

package com.neutrio.calculator;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    @Test
    public void canAddZeroPlusZero(){
        Calculator calc = new Calculator();
        int sum = calc.add(0,0);
        assertEquals(0, sum, "Was expecting sum of 0");

    }

    @Test
    public void canAddOnePlusOne(){
        Calculator calc = new Calculator();
        int sum = calc.add(1,1);
        assertEquals(2, sum, "Was expecting sum of 2");

    }
}

 

 

이때 에러가 발생하는 것을 볼 수 있다. 

 

 

 

 

 

main에서 0을 리턴하고 있기 때문이다. 

 

 

 

이때의 로직은 

 

1번의 메소드가 작동하면서 동시에 2번이 작동해야 한다는 것이다. 

 

=> 그렇게 되도록 main 함수를 to do the actual math here. 

 

Yet, here we should note that this kind of hard coding is under basic idea of TDD. 

 

With another methods put in, it would force us to do something more involved, without previously working test, which makes us to modify it as actual ways. 

 

 

 

 

 

간단하게 테스트 하는 방법 

 

 

 

 

한가지 짚고 갈 점은 테스트라는 것도 결국엔 시나리오 베이스이기 때문에 only as good as the scenarios that you can think of. 

 

=> TDD가 과연 필요한가에 대한 질문을 던지는 근거가 될 수

 

테스트만 전문으로 하는 포지션도 있다. 

 

 

 

 

테스트 코드를 리팩토링해보자. 

 

 

 

 

 

 

 

 

Calc 변수를 field로 올려주기 

 

 

 

 

다른 방법으로 setup 하는 것이 있다. 

 

 

 

 

차이점은 Final은 붙지 않은 field 변수를 declare 하고 

setup이라는 메소드를 만든다. 

 

여기서 annotation => @BeforeEach 

=> we can any void method to be run for SETTIING DATA UP, right before junit runs any test metod 

=> 실제 테스트 메소드가 실행되기 전에 calc = new Calculator();가 먼저 실행되는 기능 

=> then it would run this test method and ust that newly instantiated calculator which is stored in the variable 

 

그 다음에 다음 Tese method가 run하기 전에 다시 setupt으로 돌아가서 field variable을 또한 번 초기화 한다는 것이다. 

 

meaning the first instance of the calculator that was created before, would now be dereferenced => treated as garbage 

 

 

가장 큰 차이점은 setup을 쓰지 않으면 field 에서 정의된 하나의 declaration 만 사용되지만 setup을 통해서는 초기화가 가능하다는 것이다. 

 

in the case of the calculator should main state and we wanted to ensure that on each test, specifically where subsequent result is expceted, in that case using this setup method might be the smarter, giving us an opportunity 

to reset things to reset up things in whatever way that need to be done. 

 


 

 

next question is so what should we test next ? 

 

Edge case studying

 

 

MAX_VALUE로 연산이 가능한지를 테스트 해보자. 

 

 

 

@Test
public void canAddMaxIntPlusOne(){
    int sum = calc.add(Integer.MAX_VALUE,1);
    System.out.println(sum); // 2147483647
    assertEquals(Integer.MAX_VALUE+1, sum, "Was expecting sum of 2");

 

 

이 테스트의 리턴 값 -2147483648

 

이 나오는데 테스트의 결과는 맞다고 나오는 모순이 발생 

 

= logical한 doubling으로 구현되었기 때문이다. 

 

Java doc에 따르면 MAX_VALUE = 2147483647 

 

 

32bit의 제한 때문에 backward 현상이 나오면서 음수화된다. 

 

해결방법은? 

 

Room을 늘려주는 방식 => Long으로 타입을 바꿔준다. 

따라서 여기서 테스트에 롱을 처리해주면서 working correctly 하고 main에서의 오류가 발생 

 

 

It didn't pass but 테스트의 역할을 함. 

 

 


 

 

 

보통 연금(OrdinaryAnuuity)을 계산해보자. 

 

 

 

csus.edu

 

BigDecimal을 사용할 것을 고려해서 string으로 변수를 받는 메소드를 만든다. 

 

Example로 주어진 변수가 입력된 메소드 생성. 

 

 

 

 

 

@Test
public void annuityExample(){
    String answer = calc.calcAnnuity("22000", 7, ".06", 1);
    assertEquals("$184,664,43",answer);

}

 

이제 main 함수를 코딩.

 

 

이제 다시 테스트를 돌려보면 

 

 

에러 why did it fail ? 

 

typo = > 컴마 오타 

 

 

 

working ! 

 

 

 

그렇다면 이제 이 테스트셋을 기반으로 다른 문제들을 풀 수 있게 되었다. 

 

Example 2를 돌려보자. 

 

works.

 

 

 

 

테스트.java 파일에는 현재 해결되지 않은 문제가 있다 = edge problem 

 

이처럼 테스트셋에는 내비두지만 당장 main에서 해결할 것은 아닌 것들은 어떻게 처리할까? 

 

 

 

Disabled Annotation을 붙인다 

 

 

 


 

TDD를 이용한 implementing 

 

Guessing game을 만들어보자. 

 

패키지와 클래스 생성 

 

 

 

 

먼저 logic을 생각한다. 

 

Guessing game의 본질이 무엇일까? 

 

guess를 하고 옳고 그름을 판단하는 것 

 

 

 

 

guess하는 input을 받고 message를 반환하는 상황을 생각해보자. 

 

 

 

 

 

테스트 세팅 

 

package com.neutrio.game;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class GuessingGameTest {

    @Test
    public void testSimpleWinSituation() {
        GuesingGame game = new GuesingGame();
        String message = game.guess(3);
        assertEquals("You got it", message);
    }

}

 

실제 게임에서의 시나리오 : 

1) 랜덤으로 number generated

2) guessed된 number를 받는다 

3) 맞는지를 비교한다 

4) 결과를 리턴한다 

테스트 상황에서 random number를 받도록 한다. 

 

 

하드코딩하여 연결되는지 확인 

 

 

 

 

 

 

틀린 상황을 가정하기 위해 

-, +를 나눈 메소드를 형성 

 

@Test
public void testOneWrongNegGuessSituation() {
    GuesingGame game = new GuesingGame();
    String message = game.guess(-5);
    assertEquals("You din't get it", message);
}

@Test
public void testOneWrongPosGuessSituation() {
    GuesingGame game = new GuesingGame();
    String message = game.guess(-5);
    assertEquals("You din't get it", message);
}

 

game을 셋업 method로 빼준다. 

 

 

 

 

 

랜덤 number generation는 어떻게 하면 테스트할 수 있을까? 

 

 

여러 상황을 세팅하는 것인데 이렇게 simplify 해보자. 

 

랜덤 number가 정상적으로 작동한다면 100번을 돌렸을 때 최소 값 = 10, 최대 값은 1000이 될 것이다. 

 

 

그보다 쉽게 생각해보면 100번을 돌리면 최소한 한 번씩은 숫자가 나올 것으로 가정 

=> 1로 치환해놓고 그것을 더하면 10개가 나와야 한다는 세팅 

 

 

(사실 가장 완벽한 방법은 통계적 계산으로 분포를 구해서 해서 match시키는 방법일것이다) 

 

@Test
public void testRandomNumberGeneration() {
    // 1  2  3  4  5  6  7  8  9  10
    // 1  1  1  1  0  1  1  1  0  1  = 10

    int[] rndNumCount = new int[11];   //give us new array of ten integers
    for (int counter=0; counter < 100; counter++){
        int randomNum = game.getRandomNumber();
        rndNumCount[randomNum] = 1; // 1로 저장하고 => 모든 것을 sum 했을 때 10을 리턴해야 함
    }
    int sum=0;
    for (int counter =0; counter < 11; counter++){
        sum += rndNumCount[counter];
    }
    System.out.println(sum);
    assertEquals(10, sum);
}

 

이때 결과값은 

 

 

나오는데 이유는 main에서 구현이 안되어있고 리턴 0을 하도록 되어있기 때문이다. 

 

main에서 random 기능을 구현해보자. 

 

 

 

public int getRandomNumber(){
    return new Random().nextInt(10) + 1;
}

 

전체 코드를 한번 살펴보자. 

 

여기서 recurssing이 일어나고 있다. 

 

 

 

 

 

또 한번 새로운 random number가 generated 되면서 문제가 발생

 

now quesion is when should random number be generated ? 

 

 

 

여기서의 기능을 field로 빼주어서 반복되지 않도록 한다. 

 

 

 

 

 

테스트에서 for문을 돌릴때 local로 만들어서 분리된 상황에서 테스트가 가능하도록 한다. 

 

 

 

 

 

100이라는 숫자가 너무 크니까 조금씩 줄여가보도록 하자. 

 

 

 

 

Rerun하는 기능으로 돌려보기 

 

 

 

 

@Test
public void testRandomNumberGeneration() {
    // 1  2  3  4  5  6  7  8  9  10
    // 1  1  1  1  0  1  1  1  0  1  = 10

    int[] rndNumCount = new int[11];   //give us new array of ten integers
    for (int counter=0; counter < 80; counter++){
        GuessingGame game = new GuessingGame();
        int randomNum = game.getRandomNumber();
        rndNumCount[randomNum] = 1; // 1로 저장하고 => 모든 것을 sum 했을 때 10을 리턴해야 함
    }
    int sum=0;
    for (int counter =0; counter < 11; counter++){
        sum += rndNumCount[counter];
    }
    System.out.println(sum);
    assertEquals(10, sum);
}

 

여기서의 로직 

=> 

 

10개의 room이 있는 배열이 생성됨 

 

10개의 랜덤되는 것이 나올 것인데 예를 들어

0이면 array[0] => 1로 설정 

1이면 array[1] => 1로 설정 

 

random으로 추출하는 횟수가 많아질 수록 10개의 숫자가 다 나올 것이고 따라서 array의 room이 전부 1로 설정될 것이라는 가정 

 

따라서 그 후에 sum을 해보면 10이 나와야 한다는 가정 

 

 

There are multiple ways that we could've achieved this. 

 

 

 

4번의 guess 이후 종료시키는 기능을 테스트부터 구현해보자.

 

 

 

 

 

이때 main 함수로 돌아와서 생각해보면 count가 필요하다. 

 

 

간단하게 일단 이렇게 구현. 

 

 

 

 

3개는 틀리고 1개는 맞는 상황을 가정하기 위해 좀 더 코드를 진화시켜보자. 

 

 

 

 

btw, this is just doing bare minimun if this engine is working. 

 

 

 

 

이제 try를 다르게 할 때마다 다른 string을 내는 것을 만들어보자. 

 

 

먼저 처음에 만든 guess logic을 1 트라이로 바꿔주고 나머지도 변경 

 

 

 

메인에서 string을 변수로 처리 

 

 

이때 영어 단어 복수와 단수의 구별을 위해 조건문으로 구체화 

 

 

전부 맞는 것을 확인한다. 

 

 

 

 

 

 

4개 이상의 guesses를 핸들링하기 

 

먼저 main에서 하드코드되어 있는 이부분을 수정해보자. 

 

 

 

 

 

이제 4개 이상의 wrong에 대한 부분 구현. 

 

 

 

로직은 

 

여기서 로직 업데이트가 필요하다.

 

로직 업데이트 

 

 

 

문제는 여기서 return이 너무 많다는 것이다. 

 

we just need one master string to be returned 

 

리턴값을 response로 처리하고 하나의 return을 만들자. 

 

 

now here any of the conditions become exclusive , which then regardless of what happened at the end, one of thse response strings should have been set. 

 

public String guess(int guessedNumber) {
    counter ++;

    String tryText = counter == 1 ? "try" : "tries";
    String winningMsg = String.format("You got it in %d %s",counter, tryText);
    String response = null;

    if (counter==4 && guessedNumber != getRandomNumber()){
        response = String.format("You didn't get it and you've had %d %s. Gave over.",counter,tryText);
    } else if (counter > 4){
        response =  "Sorry, you are limited to only 4 tires. Your game is over.";
    } else {
   	 	response = guessedNumber == getRandomNumber() ? winningMsg : "You didn't get it";
    }
    return response;
}

 

 

Resulting in return proper response. 

 

 

 

좀 더 업그레이드 해보기. 

 

guess에 힌트를 주는 기능을 구현해보자. 

 

먼저 마이너스 인 부분에서 

 

"you're too low" 부분을 추가 

 

 

main을 업데이트 

 

 

 

 

 

 

참고 : ctrl + shift +J = > join 기능 

 

 

 

high 까지 구현 

 

if (counter==4 && guessedNumber != getRandomNumber()){
    response = String.format("You didn't get it and you've had %d %s. Gave over.",counter,tryText);
} else if (counter > 4){
    response =  "Sorry, you are limited to only 4 tires. Your game is over.";
} else {
        String tooHighLowText = null;
    if (guessedNumber < getRandomNumber()){
        tooHighLowText = "- you're too low";
    } else if (guessedNumber > getRandomNumber()){
        tooHighLowText = "- you're too high";
    } else {
        tooHighLowText = "";
    }
    String loseText = String.format("You didn't get it %s", tooHighLowText).trim();
    response = guessedNumber == getRandomNumber() ? winningMsg : loseText;
}
return response;

 

 

 


 

 

 

다양한 Assertions 

 

 

 

 

 


 

간단한 유저인터페이스 구현하기

 

 

메인 함수에서 loop를 돌면서 input을 받고 

output이 종료시키는 환경에 들어가면 loop를 종료시킨다 

 

public static void main(String[] args) {
    GuessingGame game = new GuessingGame();
    boolean loopShouldContinue = true;
    do{
        String input = System.console().readLine("Enter a number:");
        if ("q".equals(input)){
            return;
        }
        String output = game.guess(Integer.parseInt(input));
        System.out.println(output);
            if (output.contains("You got it") || output.contains("over")){
                loopShouldContinue = false;
            }

    } while (loopShouldContinue);
}

 

 

 

 

 

 

 

 

 

반응형