본문 바로가기
Back-End/백기선님의 자바 스터디

4주차 - 제어문

by 7533ymh 2022. 3. 16.

목표

자바가 제공하는 제어문을 학습하세요.

학습할 것 (필수)

  • 선택문
  • 반복문

과제 (옵션)

과제 설명

과제 0. JUnit 5 학습하세요.

  • 인텔리J, 이클립스, VS Code에서 JUnit 5로 테스트 코드 작성하는 방법에 익숙해 질 것.
  • 이미 JUnit 알고 계신분들은 다른 것 아무거나!
  • 더 자바, 테스트 강의도 있으니 참고하세요~

과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.

  • 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
  • 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
  • Github 자바 라이브러리를 사용하면 편리합니다.
  • 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.

과제 2. LinkedList를 구현하세요.

  • LinkedList에 대해 공부하세요.
  • 정수를 저장하는 ListNode 클래스를 구현하세요.
  • ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
  • ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
  • boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.

과제 3. Stack을 구현하세요.

  • int 배열을 사용해서 정수를 저장하는 Stack을 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

과제 4. 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.

  • ListNode head를 가지고 있는 ListNodeStack 클래스를 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

과제 5. Queue를 구현하세요.

  • 배열을 사용해서 한번
  • ListNode를 사용해서 한번.

1. 선택문

조건문의 경우 조건식과 문장을 포함하는 블럭({})으로 구성됭 있으며, 조건식의 연산결과에 따라 실행할 문장이 달라져서 프로그램의 실행흐름을 변경할 수 있다.

if문

public static void main(String[] args) {
    int score = 80;

    if (score > 70) {
        System.out.println("합격입니다.");
    }
}
=========================================
합격입니다.

조건식(if)이 참이면 괄호 안의 문장들을 수행하는 문이다.

if (score > 70)
    System.out.println("합격입니다.");

if (score > 70) System.out.println("합격입니다.");

블럭 안의 문장이 하나뿐 일때 위와 같이 괄호를 생략하거나 한줄로 쓸 수 있다.

if-else문

public static void main(String[] args) {
    int input = 1;

    if (input == 0) {
        System.out.println("0입니다.");
    } else {
        System.out.println("0이 아닙니다.");
    }
}
======================================
0이 아닙니다.

조건식의 결과가 참이 아닐 때, 즉 거짓일 때 else블럭의 문장을 수행하라는 뜻이다.

if문과 마찬가지로 블럭 내의 문장이 하나뿐인 경우 괄호를 생략할 수 있다.

if-else if문

public static void main(String[] args) {
    int score = 70;

    if (score >= 90) {
        System.out.println("A");
    } else if (score >= 80) {
        System.out.println("B");
    } else if (score >= 70) {
        System.out.println("C");
    } else {
        System.out.println("D");
    }
}

=====================================
C

처리해야할 경우의 수가 셋 이상인 경우에 사용한다.

else 블럭은 생략 가능하다.

중첩 if문

public static void main(String[] args) {
    int num = 0;

    if (num >= 0) {
        if (num == 0) {
            System.out.println("0입니다.");
        } else {
            System.out.println("양수입니다.");
        }
    }
}
==============================================
0입니다.

if문 블럭 내에 또 다른 if문을 포함시키는 문이다.

switch문

if문의 경우 조건식의 결과가 참과 거짓 두 가지뿐이기에 경우의 수가 많아질수록 else-if가 추가되야해서 복잡해질 수 잇고 여러 조건식을 처리해야 해서 처리시간도 더 길다.

switch문은 단 하나의 조건식으로 많은 경우의 수를 처리할 수 있기 때문에 처리할 경우의 수가 많을수록 if문보다는 switch문을 사용하는 게 좋다.

switch (조건식) {
    case 값1:
        //조건식의 결과가 값1과 같을 경우 수행될 코드
        break;
    case 값2:
        //조건식의 결과가 값2과 같을 경우 수행될 코드
        break;
    default:
        //조건식의 결과와 일치하는 case문이 없을 경우 수행될 코드
}

코드에서 조건식의 결과와 일치하는 값이 있는 case가 있는지 찾아서 찾는다면 해당 케이스의 코드를 수행하고, 만일 적절한 값을 찾지 못한다면 default의 코드를 수행한다. 여기서 각각의 케이스는 코드 수행 후 break문을 만나 전체 switch문을 빠져나가는데 만약 break문이 없다면 종료되지 않고 계속 진행이 되며 이것을 폴 스루(fall - through) 라고 한다.

추가적으로 default는 보통 마지막에 놓기 때문에 break문을 생략하는 경우도 있다.

하지만, 고의적으로 break문을 생략하는 경우도 있다. 예를 들어, 회원제로 운영되는 웹 사이트에서 사용자의 등급(level)을 체크하여 등급에 맞는 권한을 부여하는 방식으로 구현하는 경우 break문을 생략할 수 도 있다.

public static void main(String[] args) {
    int level = 2;

    switch (level) {
        case 3:
            grandDelete(); // 삭제권한
        case 2:
            grantWrite(); // 쓰기권한
        case 1:
            grantRead(); // 읽기권한
    }
}

위와 같이 사용한다면 레벨에 따라 3인 사람은 읽기, 쓰기, 삭제 권한을 모두 가지고, 레벨 2는 쓰기, 읽기 권한만, 레벨1은 읽기 권한만 가지도록 할 수 있다.

switch문의 제약조건

  • switch문의 조건식 결과는 정수 또는 문자열이어야 한다.
  • case문의 값은 정수 상수만 가능하며, 중복되지 않아야 한다.
int num, result;
final int ONE = 1;

switch(condition){
    case '1':   //OK. 문자 리터럴(정수 상수 49와 동일)
    case ONE:   //OK. 정수 상수
    case "YES": //OK. 문자열 리터럴, JAVA 7부터 허용
    case num;   //에러. 변수는 불가
    case 1.0:   //에러. 실수는 불가
}

if문과 switch문

  • if문
if (month == 3 || month == 4 || month == 5) {
    System.out.println("봄");
} else if (month == 6 || month == 7 || month == 8) {
    System.out.println("여름");
} else if (month == 9 || month == 10 || month == 11) {
    System.out.println("가을");
} else { // month == 12 || month == 1 || month == 2
    System.out.println("겨울");
}
  • switch문
switch (month) {
    case 3: case 4: case 5:
        System.out.println("봄");
        break;
    case 6: case 7: case 8:
        System.out.println("여름");
        break;
    case 9: case 10: case 11:
        System.out.println("가을");
        break;
    default: //case 12: case 1: case 2
        System.out.println("겨울");
}

이런 경우 if문 보다 switch문이 한결 간결한 표현이 된다. 또한, case문은 한 줄에 하나씩 쓰던, 한 줄에 붙여서 쓰던 상관없다.

switch문의 중첩사용

switch문 역시 중첩사용이 가능하다. 주의할 점은 중첩 switch문에서 break문을 빼먹기 쉽다는 것이다.

public static void main(String[] args) {
    char gender = '3';

    switch (gender) {
        case '1':
        case '3':
            switch (gender) {
                case '1':
                    System.out.println("2000년 이전에 출생한 남자");
                    break;
                case '3':
                    System.out.println("2000년 이후에 출생한 남자");
            }
            break; //주의!!
        case '2':
        case '4':
            switch (gender) {
                case '2':
                    System.out.println("2000년 이전에 출생한 여자");
                    break;
                case '4':
                    System.out.println("2000년 이후에 출생한 여자");
            }
            break;
        default:
            System.out.println("유효하지 않은 주민등록번호입니다.");
    }
}

2. 반복문

반복문은 어떤 작업을 반복수행하기 위해 사용된다.

  • for
  • while
  • do-while

forwhile의 경우 조건에 따라 한 번도 수행되지 않을 수 있지만 do-while문에 속한 문장은 무조건 최소한 번은 수행될 것이 보장된다.

for문

반복 횟수를 알고 있을 때 적합하다.

for (초기화; 조건식; 증감식) {
    // 조건식이 참일때 수행될 문장들을 적는다.
}
        ①         ②     ③
for (int i = 1; i <= 5; i++) {
    수행될 코드
}
  1. 초기화 ① : for문의 기준이 될 변수의 초기값 설정으로 위 코드에서는 int타입의 i1을 설정한다.
  2. 조건식 ② : for문이 수행될 조건으로 해당 조건이 true가 되는 동안 반복이 수행된다.
  3. 증감식 ③ : 코드 수행 후 조건평가 전 수행되며, 위 코드에서는 i를 1씩 후위증가 시킨다.

for문 수행순서

        ①         ②     ④
for (int i = 1; i <= 5; i++) {
    수행될 코드 ③
}

초기화 ①가 먼저 수행되고, 그 이후부터는 조건식이 참인 동안 조건식 ② -> 수행될 코드 ③->증감식 ④의 순서로 계속 반복되고 조건식이 거짓이 되면 for문 전체를 빠져나가게 된다.

중첩 for문

for문 안에 또 다른 for문을 포함시키는 것이 가능하다.

public static void main(String[] args) {
    for (int i = 2; i <= 9; i++) {
        for (int j = 1; j <= 9; j++) {
            System.out.printf("%d X %d = %d\n", i, j, i * j);
        }
    }
}

===========================================
2 X 1 = 2
2 X 2 = 4
2 X 3 = 6
2 X 4 = 8
......
9 X 8 = 72
9 X 9 = 81

향상된 for문 (enhanced for statement)

JDK 1.5부터 배열과 컬렉션에 저장된 요소에 접근할 때 기존보다 편리한 방법으로 처리할 수 있도록 for문의 새로운 문법이 추가되었다.

for( 타입변수명 : 배열 또는 컬렉션 ) {
    //반복될 코드
}

위의 코드에서 타입은 배열 또는 컬렉션의 요소의 타입이어야 한다.

public static void main(String[] args) {
    String[] names = new String[] {"pobi", "min", "hwan"};

    for (String name : names) {
        System.out.println(name);
    }
}

==========================================
pobi
min
hwan
  • 인텔리제이에서 iter 명령어를 입력하면 향상된 for문을 사용할 수 있다. 일반적인 for문의 경우 itar명령어를 입력하면 된다.

while문

while (조건식) {
    //조건식의 연산결과가 참(true)인 동안, 반복될 코드들을 적는다.
}

if문과 유사하지만 while문은 조건식이 참(true)인동안 블럭내의 문장을 반복한다.

조건식이 참일 경우 블럭안으로 들어가서 문장을 수행하고 다시 조건식으로 돌아가고 거짓일 경우 while문을 벗어난다.

public static void main(String[] args) {
    int i = 2;
    int j = 1;

    while (i <= 9) {
        while (j <= 9) {
            System.out.printf("%d X %d = %d\n", i, j, i * j);
            j++;
        }
        i++;
        j = 1;
    }
}

============================
2 X 1 = 2
2 X 2 = 4
2 X 3 = 6
2 X 4 = 8
......
9 X 8 = 72
9 X 9 = 81

for문과 while문

상황에 따라 초기화나 증감식이 필요하지 않은 경우 while문을 사용하고 그렇지 않다면 for문을 사용하면 된다.

while문의 조건식은 생략불가

for문과 다르게 while문의 조건식은 생략할 수 없다. 그렇기에 true를 넣어 항상 참이 되도록 만들어 줘야 한다.

while () {
    //수행될 코드
}

for (;;) { //조건식이 항상 참
    //수행될 코드
}

do-while문

기본적인 구조는 while문과 같으나 조건식과 블럭의 순서를 바꾼것으로, 기존의 while문과는 반대로 블럭을 먼저 수행 후 조건식을 평가한다.

그렇기에 다른 반복문과 다르게 최소한 한번은 수행될 것을 보장한다.

do {
    //조건식의 연산결과가 참일때 수행될 문장들을 적는다.
} while (조건식);
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int input = 0;
    int answer = 0;

    answer = new Random().nextInt(10);

    do {
        System.out.println("1과 10사이의 정수를 입력하세요.");
        input = scanner.nextInt();

        if (input > answer) {
            System.out.println("더 작은 수로 다시 시도해보세요.");
        } else if (input < answer) {
            System.out.println("더 큰 수로 다시 시도해보세요.");
        }
    } while (input != answer);

    System.out.println("정답");
}

======================================
1과 10사이의 정수를 입력하세요.
2
더 작은 수로 다시 시도해보세요.
1과 10사이의 정수를 입력하세요.
1
정답

break문

break문은 자신이 포함된 가장 가까운 반복문을 벗어난다. 그렇기에 보통 if문과 함께 사용되어 특정 조건을 만족하면 반복문을 벗어나도록 한다.

  • 숫자를 1부터 계속 더해 나가서 몇까지 더하면 합이 100을 넘는지 확인하는 예제
public static void main(String[] args) {
    int sum = 0;
    int i = 0;

    while (true) {
        if (sum > 100) {
            break;
        }
        ++i;
        sum += i;
    }

    System.out.printf("i = %d, sum = %d\n", i, sum);
}

continue문

continue문은 반복문 내에서만 사용되며, 반복이 진행되는 도중에 continue문을 만나면 반복문의 끝으로 이동하여 다음 반복으로 넘어간다. for문의 경우 증감식으로 이동하며 while문과 do-while문은 조건식으로 이동한다.

반복문을 벗어나지 않고 다음 반복을 수행할 수 있기에 주로 if문과 함께 사용되여 특정 조건을 만족할 경우 continue문이 다음 코드를 수행하지 않고 다음 반복으로 넘어가서 진행하도록 한다.

  • 홀수만 출력하는 에제
public static void main(String[] args) {
    for (int i = 1; i < 30; i++) {
        if (isEven(i)) {
            continue;
        }
        System.out.println(i);
    }
}

public static boolean isEven(int num) {
    return num % 2 == 0;
}

======================================
1
3
5
...
27
29

이름 붙은 반복문

break문은 근접한 하나의 반복문만 벗어날 수 있기에, 여러 반복문이 중첩된 경우 break문으로 중첩 반복문을 완전히 벗어날 수 없다. 이때는 중첩 반복문 앞에 이름을 붙혀 break문과 continue문에 이름을 지정해 하나의 반복문을 벗어나거나 건너뛸 수 있다.

public static void main(String[] args) {
    Loop1:
    for (int i = 2; i <= 9; i++) {
        for (int j = 1; j <= 9; j++) {
            if (i + j > 6) {
                continue Loop1;
            }

            System.out.printf("%d X %d = %d\n", i, j, i * j);
        }
        System.out.println();
    }
}

====================================
2 X 1 = 2
2 X 2 = 4
2 X 3 = 6
2 X 4 = 8
3 X 1 = 3
3 X 2 = 6
3 X 3 = 9
4 X 1 = 4
4 X 2 = 8
5 X 1 = 5
public static void main(String[] args) {
    Loop1:
    for (int i = 2; i <= 9; i++) {
        for (int j = 1; j <= 9; j++) {
            if (j == 4) {
                continue Loop1;
            }

            System.out.printf("%d X %d = %d\n", i, j, i * j);
        }
        System.out.println();
    }
}

===========
2 X 1 = 2
2 X 2 = 4
2 X 3 = 6
3 X 1 = 3
....
9 X 1 = 9
9 X 2 = 18
9 X 3 = 27

과제 0. JUnit5 학습

JUnit이란?

  • 자바 개발자의 93%가 사용하는 단위 테스트 프레임워크
  • 스프링 부트 2.2버전 이상부터 기본 제공

JUnit5 란?

JUnit5 = Junit Platform + Junit Jupiter + Junit Vintage

JUnit5에서는 이전 버전의 Junit과는 다르게 3가지 하위 프로젝트의 여러 모듈로 구성된다.

  • Platform : 테스트를 실행해주는 런처 제공. TestEngine API 제공
  • Jupiter : JUnit 5를 지원하는 TestEngine API 구현체
  • Vintage : JUnit 4와 3을 지원하는 TestEngine 구현체

추가적으로 JAVA 8부터 지원하며, 이전 버전으로 컴파일된 코드는 계속 테스트가 가능하다.

JUnit5 설정

  • 스프링 부트 프로젝트

    • 스프링 부트 2.2버전 이상부터는 기본적으로 JUnit5 의존성이 추가되어 있다.
  • 스프링 부트 프로젝트가 아닐 경우

    • 아래와 같이 의존성을 추가해준다.

      • Maven

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
      • Gradle

        testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
      • 의존성 사이트 : https://mvnrepository.com

테스트 클래스와 메서드

테스트 클래스

  • 최상위 클래스, 스태틱 멤버 클래스, @Nested 클래스에 적어도 한개의 @Test어노테이션이 달린 테스트 메서드가 포함되어 있는 것을 말한다.
  • 테스트 클래스는 abstract이면 안되고, 하나의 생성자가 있어야 한다. ( 어차피 생성자가 없을 경우 컴파일러가 자동으로 만들어준다.)

테스트 메서드

  • @Test ,@RepeatedTest ,@ParamterizedTest,@TestFactory,@TestTemplate 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.

라이플사이클 메서드

  • @BeforeAll , @AfterAll , @BeforeEach , @AfterEach 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.

테스트 메서드와 라이플사이클 메서드는 테스트 할 클래스, 상속한 부모클래스 또는 인터페이스에 선언된다. 추가로 테스트 메서드와 라이플사이클 메서드는 abstract선언하면 안되고, 어떠한 값도 리턴되선 안된다.

또한, 접근제어자를 public으로 선언을 꼭 안해줘도 되지만 private로 선언하면 안된다.

Junit5 Annotations

기본 어노테이션

  1. @Test

    • 테스트 메서드라는 것을 나타내는 어노테이션
    • Junit4와 다르게 어떠한 속성도 선언하지 않는다.
    • Junit Jupiter의 테스트 확장 프로그램은 자체 전용 주석을 기반으로 작동하기 때문이다.
    • 접근제한자가 Default여도 된다. (Junit4 까지는 public이어야 했다.)
  2. @BeforeAll / @AfterAll

    • 해당 클래스에 위치한 모든 테스트 메서드 실행 전/후에 딱 한번 실행되는 메서드
    • static void 여야 한다.
    • JUnit4의 @BeforeClass / @AfterClass 와 유사
  3. @BeforeEach / @AfterEach

    • 해당 클래스에 위치한 모든 테스트 메서드 실행 전/후에 실행되는 메서드
    • JUnit4의 @Before / @After와 유사
    • 매 테스트 메서드마다 새로운 클래스를 생성(new)하여 실행 (비효율적)
  • @BeforeAll@BeforeEach 모두 여러 개의 테스트 조건을 setup할 때 사용한다.
    • @BeforeAll의 경우 한 번만 실행되므로 테스트가 조건들에 대해 어떠한 변경도 하지 않는다는 확신이 있을 때 사용한다.
    • @BeforeEach의 경우 여러번 실행되므로 테스트가 조건들에 영향을 미친다면 매 테스트 실행 때마다 조건들이 초기화시킬 때 사용한다.
  1. @Disabled
    • 테스트를 하고 싶지 않은 클래스나 메서드에 붙이는 어노테이션
    • JUnit4의 @Ignore과 유사
class JunitTest {

    @BeforeAll
    static void beforeAll() {
        System.out.println("beforeAll");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("afterAll");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("beforeEach");
    }

    @AfterEach
    void afterEach() {
        System.out.println("afterEach");
    }

    @Test
    void test1() {
        System.out.println("test1");
    }

    @Test
    void test2() {
        System.out.println("test2");
    }

    @Disabled
    @Test
    void test3() {
        System.out.println("test3");
    }
}

1

  1. @RepeatedTest
    • 특정 테스트를 반복시키고 싶을 때 사용하는 어노테이션
    • 반복 횟수와 반복 테스트 이름을 설정가능
class JunitTest {
    @RepeatedTest(10)
    @DisplayName("횟수만 지정한 반복 테스트")
    void repeatedTest() {
        System.out.println("횟수 지정 반복 테스트");
    }

    @RepeatedTest(value = 10, name = "{displayName} 중 {currentRepetition} of {totalRepetitions}")
    @DisplayName("이름도 지정한 반복 테스트")
    void repeatedTest2() {
        System.out.println("이름도 지정한 테스트");
    }
}

4

2

  1. @ParameterizedTest

    • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행할 때 사용하는 어노테이션
    public class Junit {
        public static boolean isOdd(int number) {
            return number % 2 != 0;
        }
    }
    
    class JunitTest {
        @ParameterizedTest
        @ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE})
        void isOdd_ShouldReturnTrueForOddNumbers(int number) {
            Assertions.assertTrue(Junit.isOdd(number));
        }
    }

    5

    • 만약 JUnit5 의존성 추가시 junit-jupiter-api만 추가했을 경우 junit-jupiter-params도 추가해줘야지 사용가능하다.
    testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.8.2'
    
    // api와 params를 한번에 추가할 경우 아래와 같이 추가해주면 된다.
    testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.8.2'
  1. @Nested

    • 테스트 클래스 안에서 내부 클래스를 정의해 테스트를 계층화할 때 사용한다.
    • 내부클래스는 부모클래스의 멤버 필드에 접근 가능하다.
    • Before / After와 같은 테스트 생명주기에 관계된 메소드들도 계층에 맞춰 동작한다.
    public class Junit {
        public static boolean isOdd(int number) {
            return number % 2 != 0;
        }
    
        public static boolean isEven(int number) {
            return number % 2 == 0;
        }
    }
    
    class JunitTest {
    
        @DisplayName("isOdd 메서드는")
        @Nested
        class isOdd {
    
            @DisplayName("숫자가 홀수일 때 true를 반환한다.")
            @Test
            void returnTrue() {
                Assertions.assertTrue(Junit.isOdd(1));
            }
    
            @DisplayName("숫자가 짝수일 때 false를 반환한다.")
            @Test
            void returnFalse() {
                Assertions.assertFalse(Junit.isOdd(2));
            }
    
        }
    
        @DisplayName("isEven 메서드는")
        @Nested
        class isEven {
    
            @DisplayName("숫자가 홀수일 때 false를 반환한다.")
            @Test
            void returnTrue() {
                Assertions.assertTrue(Junit.isEven(2));
            }
    
            @DisplayName("숫자가 짝수일 때 true를 반환한다.")
            @Test
            void returnFalse() {
                Assertions.assertFalse(Junit.isEven(1));
            }
    
        }
    }

  2. @TestFactory

    • @Test로 선언된 정적 테스트가 아닌 동적으로 테스트를 사용한다.
  3. @TestInstance

    • 테스트 클래스의 생명주기를 설정한다.
  4. @TestTemplate

    • 공급자에 의해 여러 번 호출될 수 있도록 설계된 테스트 케이스 템플릿임을 나타낸다.
  5. @TestMethodOrder

    • 테스트 메서드 실행 순서를 구성하는 데 사용한다.
  6. @Tag

    • 클래스 또는 메서드 레벨에서 태그를 선언할 때 사용한다.
  7. @Timeout

    • 테스트 실행 시간을 선언 후 초과되면 실패하도록 설정한다.
  8. @ExtendWith

    • 확장을 선언적으로 등록할 때 사용한다.
  9. @RegisterExtension

    • 필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용한다.
  10. @TempDir

    • 필드 주입 또는 매개변수 주입을 통해 임시 디렉토리를 제공하는 데 사용한다.

테스트 명 표시 어노테이션

  1. @DisplayName

    • 어떤 테스트인지 쉽게 표현할 수 있도록 해주는 어노테이션
    • 공백, Emoji, 특수문자 등을 모두 지원
  2. @DisplayNameGeneration

    • 테스트 클래스에 대한 사용자 정의 표시 이름 생성기를 선언한다.
    • Method와 Class 래퍼런스를 사용해 테스트 이름을 표기하는 방법 설정으로 기본 구현체로 ReplaceUnderscores를 제공한다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class Junit_Test {
    @Test
    @DisplayName("이것은 DisplayName 테스트입니다.")
    void test() {
    }
}

6

Assertions

  • 테스트 케이스의 수행 결과를 판별하는 메서드이다.
  • 모든 Junit Jupiter Assertions는 static 메서드이다.
  1. assertEquals(expected, actual, (message OR Supplier))

    • 실제 값이 기대한 값과 같은지 확인
    • 인자를 2개만 사용할 시 그 두개가 같은 값인지 비교
    • 인자를 3개를 사용할 때 테스트가 실패하였을 경우 3번째 인자를 메시지로 출력
    • 세번째 인자에 String이나 Supplier을 넣어줄 수 있는 데 String의 경우 테스트가 실패하든 성공하든 매번 메시지를 생성하지만, Supplier는 실패할 때만 메시지를 생성한다.
    • 조그마한 성능차이에도 불안하다면 Supplier를 사용하자.
    • 이는 assertTrue(), assertNotNull()도 마찬가지
    public class Study {
    
        private final StudyStatus status;
        private final int limit;
        private final int numberOfMinEnrolment;
    
        public Study(int limit, int numberOfMinEnrolment) {
            if (numberOfMinEnrolment < 0) {
                throw new IllegalArgumentException("최소 참석인원은 0 보다 커야 합니다.");
            }
            status = StudyStatus.DRAFT;
            this.limit = limit;
            this.numberOfMinEnrolment = numberOfMinEnrolment;
        }
    
        public StudyStatus getStatus() {
            return status;
        }
    
        public int getLimit() {
            return limit;
        }
    
        public int getNumberOfMinEnrolment() {
            return numberOfMinEnrolment;
        }
    }
    
    public enum StudyStatus {
        DRAFT
    }
    
    class JunitTest {
    
        @Test
        void messageTest() {
            Study study = new Study(-50, 0);
            assertEquals(StudyStatus.DRAFT, study.getStatus(), "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.");
            assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.");
            assertEquals(StudyStatus.DRAFT, study.getStatus(), new Supplier<String>() {
                @Override
                public String get() {
                    return "처음 스터디를 만들면 상태값은" + StudyStatus.DRAFT + "여야 한다.";
                }
            });
        }
    }

    7

    • 실패할 경우

      8

  1. assertNotNull(actual)

    • 값이 null이 아닌 지 확인
  2. assertTrue(boolean)

    • 다음 조건이 참인지 확인
public class Calculator {

    public int add(int numberA, int numberB) {
        return numberA + numberB;
    }

    public int multiply(int numberA, int numberB) {
        return numberA * numberB;
    }

    public Integer divide(int numberA, int numberB) {
        if (numberB == 0) {
            return null;
        }
        return numberA / numberB;
    }
}

class JunitTest {

    @Test
    void standardAssertions() {
        Calculator calculator = new Calculator();

        assertEquals(2, calculator.add(1, 1));
        assertEquals(4, calculator.multiply(2, 2),
            "The optional failure message is now the last parameter");
        assertNotNull(calculator.divide(1, 1));
        assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
            + "to avoid constructing complex messages unnecessarily.");
    }
}

9

  1. assertAll(executables ...)

    • 매개변수로 받는 모든 테스트코드를 한번에 실행

    • 오류가 나도 끝까지 실행한 뒤 한 번에 모아서 출력

    • 인자로 람다식을 사용하며, 여러 개의 람다식이 동시에 실행되는 특징

    • 원래 assertions는 assertion이 실패하면 그 밑의 코드는 더 이상 진행하지 않는다.

      //위 Study 클래스 예제에 생성자 추가
      public Study() {
              this.status = null;
              this.limit = -1;
              this.numberOfMinEnrolment = 0;
      }
      
      @Test
      void create_study() {
          Study study = new Study();
          assertAll(
              () -> assertNotNull(study),
              () -> assertEquals(StudyStatus.DRAFT, study.getStatus(),
                  () -> "스터디를 처음 만들면 " + StudyStatus.DRAFT + "상태 여야 합니다."),
              () -> assertTrue(study.getLimit() > 0, "스터디 최대 참석 가능 인원은 0보다 커야 합니다.")
          );
      }

      10

  1. assertThrows(expectedType, executable)

    • 예외 발생을 확인하는 테스트

    • executable의 로직이 실행하는 도중 expectedType의 에러를 발생시키는 지 확인

    • 예외 발생을 검증하고 예외를 반환 받아서 예외의 상태까지 검증 가능

    • 추가적으로 예외가 던져지지 않음을 검증하는 단정문인 assertDoesNotThrow도 Junit 5.2부터 추가됨.

      @Test
      void exceptionThrows() {
          Exception e = assertThrows(Exception.class, () -> new Study(10, -10));
          // IllegalArgumentException illegalArgumentException =
          //                 assertThrows(IllegalArgumentException.class, () -> new Study(10, -1));
          String message = e.getMessage();
          assertEquals("최소 참석인원은 0 보다 커야 합니다.", message);
      
          assertDoesNotThrow(() -> new Study(10, 5));
      }

      11

  1. assertTimeout(duration, executable)

    • 특정 시간 안에 실행이 완료되는 지 확인

    • Duration : 원하는 시간

    • Executable : 테스트할 로직

    • 주의할 점은 원하는 시간이 지나자마자 테스트가 실패하는 게 아니라, Executable 코드가 모두 완료된 후에야 테스트 성공/실패를 결정짓는다.

    • 이와 반대로 assertTimeoutPreemptively()는 원하는 시간 안에 실행이 끝나지 않으면 바로 종료해 버린다.

      public class TimeOutExample {
      
          @Test
          @DisplayName("타임아웃 준수")
          void timeOutNotExceeded() {
              assertTimeout(Duration.ofMinutes(2), () -> {
                  new Study(10, 10);
                  Thread.sleep(10);
              });
          }
      
          @Test
          @DisplayName("타임아웃 초과")
          void timeOutExceeded() {
              assertTimeout(Duration.ofMillis(10), () -> {
                  new Study(10, 10);
                  Thread.sleep(100);
              });
          }
      }
      
      //Duration의 경우 static으로 import해서 사용하는 것이 더 깔끔하다.

      12

Assumption

  • 특정 조건(환경)을 전제하고 테스트를 수행할 수 있음
  • 즉, 전제문이 true라면 실행, false라면 종료
  • CI와 같이 특정 환경에서만 테스트를 진행해야 하는 경우 사용 가능
  1. assumeTrue

    • false일 때 이후 테스트 전체가 실행되지 않음
    • false일 경우 테스트가 실패하는 것이 아닌 건너 뛰게 됨
  2. assumingThat

    • 파라미터로 전달된 코드블럭만 실행되지 않음
public class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void dev_env_only() {
        assumeTrue("DEV".equals(System.getenv("ENV")), () -> "개발 환경이 아닙니다.");
        assertEquals(2, calculator.add(1, 1)); // 단정문이 실행되지 않음
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("DEV".equals(System.getenv("ENV")),
            () -> {
                assertEquals(2, calculator.divide(4, 2)); // 단정문이 실행되지 않음
            });
        assertEquals(42, calculator.multiply(6, 7)); // 단정문이 실행됨
    }
}
  • assumeTrue를 통해 전제조건을 검증한 경우 결과가 false라면 이후 테스트들이 전부 실행되지 않음.
  • assumingThat을 통해 검증한 경우 파라미터로 전달된 코드블럭만 실행되지 않고 (false인 경우) 그 이후에 테스트들은 정상적으로 수행하게 됨.
  • dev_env_only()의 System.out(”ENV”)는 환경변수를 가져오는 메서드이다.
    • 해당 메서드를 통해 현재 환경변수를 가져오고, 이것이 DEV와 같다면 테스트를 실행하게 된다.

과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.

  • 깃헙 이슈 1 ~ 18번까지 댓글 순회 후 댓글 남긴 사용자 체크
  • 참여율 계산
    • 총 18회 중 몇 %를 참여했는 지 소숫점 두자리 까지 보이기

구현

Dashboard.java
public class Dashboard {
    private static final int endIndex = 15;

    private final GHRepository repository;
    private final List<Participant> participants;

    public Dashboard(String token, String repositoryName) throws IOException {
        GitHub gitHub = new GitHubBuilder().withOAuthToken(token).build();
        this.repository = gitHub.getRepository(repositoryName);
        this.participants = new ArrayList<>();
    }

    public void printBoard() throws IOException {
        for (int index = 1; index <= endIndex; index++) {
            GHIssue issue = repository.getIssue(index);
            List<GHIssueComment> comments = issue.getComments();

            for (GHIssueComment comment : comments) {
                if (Objects.isNull(comment.getUser().getName())) {
                    continue;
                }

                Participant participant = findParticipant(participants, comment.getUser().getName());
                participant.markStatus(index);
            }
        }

        Collections.sort(participants);
        for (Participant participant : participants) {
            System.out.printf("| %s %s | %.2f%% |\n", participant.getUserName(), checkMark(participant),
                participant.getRate(endIndex));
        }
    }

    private Participant findParticipant(List<Participant> participants, String userName) {
        if (isNewUser(participants, userName)) {
            Participant participant = new Participant(userName);
            participants.add(participant);
            return participant;
        }

        return participants.stream()
            .filter(p -> p.isSameUser(userName))
            .findFirst()
            .orElseThrow(IllegalArgumentException::new);
    }

    private boolean isNewUser(List<Participant> participants, String userName) {
        return participants.stream()
            .noneMatch(p -> p.isSameUser(userName));
    }

    private String checkMark(Participant participant) {
        StringBuilder result = new StringBuilder();
        for (int index = 1; index <= endIndex; index++) {
            if (participant.isMarking(index) && participant.haveResult(index)) {
                result.append("| O ");
            } else {
                result.append("|   ");
            }
        }
        return result.toString();
    }
Participant.java
public class Participant implements Comparable<Participant> {

    private final String userName;
    private final Map<Integer, Boolean> status;

    public Participant(String userName) {
        this.userName = userName;
        this.status = new HashMap<>();
    }

    public double getRate(double total) {
        long count = status.values()
            .stream()
            .filter(value -> value)
            .count();
        return count * 100 / total;
    }

    public void markStatus(int index) {
        this.status.put(index, true);
    }

    public boolean isSameUser(String userName) {
        return this.userName.equals(userName);
    }

    public boolean isMarking(int index) {
        return status.containsKey(index);
    }

    public boolean haveResult(int index) {
        return status.get(index);
    }

    public String getUserName() {
        return userName;
    }

    @Override

    public int compareTo(Participant o) {
        return this.userName.compareTo(o.userName);
    }
}
Application.java
public class Application {
    public static void main(String[] args) throws IOException {
        Dashboard dashboard = new Dashboard("토큰", "레포지토리이름");
        dashboard.printBoard();
    }
}

결과 (일부분)

13

문제점

  1. 과제에는 1 ~ 18번까지의 이슈를 봐야하지만 16번 이슈를 호출하게 되면 에러가 발생했다.

    • 이에 확인해보니 15번 이슈 이후 16번 이슈는 없고 zeze라는 28번 이슈가 나와 생긴 오류였다...
    • 아마 스터디가 진행되면서 변경된 듯하다.
    • 이에 endIndex를 15까지 설정하여 진행하였다.
    @Test
    void DashboardTest() throws IOException {
       GitHub gitHub = new GitHubBuilder().withOAuthToken("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").build();
       GHRepository repository = gitHub.getRepository("whiteship/live-study");
    
       List<GHIssue> issues = new ArrayList<>(repository.getIssues(GHIssueState.ALL));
       issues.sort(Comparator.comparingInt(GHIssue::getNumber));
    
       for (GHIssue issue : issues) {
           System.out.println(issue.getNumber() + " " +issue.getTitle());
       }
    }

14

  • 이슈를 모두 확인하는 테스트 코드를 작성하던 중 그냥 출력하면 내림차순으로 출력해주기 위해 정렬하였지만 UnsupportedOperationException가 발생하였다.
  • 확인해보니 getIssue메서드가 toList메서드로 리스트를 반환해주는 데 이 메서드는 Collections.unmodifiableList를 사용하여 불변 리스트로 반환해주기 때문에 정렬이 불가능한 것이였다.
  • 그래서 바로 사용하지 않고 new ArrayList<>()로 한번 감싸주어 가변 리스트로 사용하여 정렬해주었다.
  1. 댓글 남긴 사용자의 이름을 가져오는 도중 에러가 발생하였다.
    • 확인해보니 댓글을 남긴 사용자가 탈퇴한 경우 null로 반환해주고 있어 null체크를 해주어 문제를 해결하였다.

과제 2. LinkedList를 구현하세요.

구현

ListNode.java
public class ListNode {
    private final int data;
    private ListNode nextNode;

    public ListNode(int data) {
        this.data = data;
        this.nextNode = null;
    }

    public void linking(ListNode nextNode) {
        this.nextNode = nextNode;
    }

    public ListNode next() {
        return nextNode;
    }

    public int getData() {
        return data;
    }
}
LinkedList.java
public class LinkedList {
    private ListNode head;
    private ListNode tail;
    private int size;

    public LinkedList() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    public ListNode add(ListNode nodeToAdd, int position) {
        if (position < 0 || position > size) {
            throw new IndexOutOfBoundsException();
        }

        if (position == 0) {
            return addFirst(nodeToAdd);
        }

        if (position == size) {
            return addLast(nodeToAdd);
        }

        ListNode prevNode = search(position - 1);
        ListNode nextNode = prevNode.next();

        prevNode.linking(nodeToAdd);
        nodeToAdd.linking(nextNode);
        size++;
        return nodeToAdd;
    }

    private ListNode addFirst(ListNode nodeToAdd) {
        nodeToAdd.linking(head);
        this.head = nodeToAdd;
        size++;

        if (head.next() == null) {
            this.tail = head;
        }

        return nodeToAdd;
    }

    private ListNode addLast(ListNode nodeToAdd) {
        tail.linking(nodeToAdd);
        tail = nodeToAdd;
        size++;
        return nodeToAdd;
    }

    private ListNode search(int position) {
        if (position < 0 || position >= size) {
            throw new IndexOutOfBoundsException();
        }

        ListNode findNode = head;

        for (int index = 0; index < position; index++) {
            findNode = findNode.next();
        }

        return findNode;
    }

    public ListNode remove(int positionToRemove) {
        if (positionToRemove < 0 || positionToRemove >= size) {
            throw new IndexOutOfBoundsException();
        }

        if (positionToRemove == 0) {
            return removeHead();
        }

        ListNode prevNode = search(positionToRemove - 1);
        ListNode removeNode = prevNode.next();
        ListNode nextNode = removeNode.next();

        prevNode.linking(nextNode);

        if (prevNode.next() == null) {
            tail = prevNode;
        }

        removeNode.linking(null);
        size--;

        return removeNode;
    }

    private ListNode removeHead() {
        ListNode headNode = head;

        if (headNode == null) {
            throw new NoSuchElementException();
        }

        ListNode nextNode = head.next();

        headNode.linking(null);
        head = nextNode;
        size--;

        if (size == 0) {
            tail = null;
        }

        return headNode;
    }

    public boolean contains(ListNode nodeToCheck) {
        ListNode findNode = head;

        while (findNode != null) {
            if (findNode.equals(nodeToCheck)) {
                return true;
            }

            findNode = findNode.next();
        }

        return false;
    }
}

테스트

LinkedListTest.java
@DisplayName("LinkedList 테스트")
class LinkedListTest {

    @DisplayName("add 메서드는")
    @Nested
    class Describe_add {

        @DisplayName("잘못된 position이 주어졌다면")
        @Nested
        class Context_with_invalid_position {
            final int FIRST_NODE_VALUE = 10;

            @DisplayName("예외를 던진다.")
            @ParameterizedTest
            @ValueSource(ints = {2, -1, 5})
            void it_throws_IndexOutOfBoundsException(int INVALID_POSITION) {
                LinkedList linkedList = new LinkedList();

                assertThrows(IndexOutOfBoundsException.class,
                    () -> linkedList.add(new ListNode(FIRST_NODE_VALUE), INVALID_POSITION));
            }
        }

        @DisplayName("유효한 position이 주어졌다면")
        @Nested
        class Context_with_valid_position {

            @DisplayName("맨 앞의 경우")
            @Nested
            class Sub_context_with_front {
                final int FIRST_NODE_VALUE = 10;
                final int FIRST_POSITION = 0;

                @DisplayName("리턴 값이 head와 같다.")
                @Test
                void it_returns_list_node_same_head() throws NoSuchFieldException, IllegalAccessException {
                    LinkedList linkedList = new LinkedList();
                    ListNode addNode = linkedList.add(new ListNode(FIRST_NODE_VALUE), FIRST_POSITION);
                    Field headField = linkedList.getClass().getDeclaredField("head");
                    headField.setAccessible(true);
                    assertEquals(addNode, headField.get(linkedList));
                }
            }

            @DisplayName("첫 번째 노드와 두 번째 노드 사이에 넣을 경우")
            @TestInstance(TestInstance.Lifecycle.PER_CLASS)
            @Nested
            class Sub_context_with_between_first_second {
                final int FIRST_NODE_VALUE = 10;
                final int SECOND_NODE_VALUE = 5;
                final int BETWEEN_NODE_VALUE = 3;
                final int FIRST_POSITION = 0;
                final int SECOND_POSITION = 1;

                ListNode first, second, between, head;

                @BeforeAll
                void prepare_add_test() throws IllegalAccessException, NoSuchFieldException {
                    LinkedList linkedList = new LinkedList();
                    first = linkedList.add(new ListNode(FIRST_NODE_VALUE), FIRST_POSITION);
                    second = linkedList.add(new ListNode(SECOND_NODE_VALUE), SECOND_POSITION);
                    between = linkedList.add(new ListNode(BETWEEN_NODE_VALUE), SECOND_POSITION);

                    Field headField = linkedList.getClass().getDeclaredField("head");
                    headField.setAccessible(true);
                    head = (ListNode)headField.get(linkedList);
                }

                @DisplayName("리턴 값이 head의 다음 노드와 같다.")
                @Test
                void it_returns_same_head_next() {
                    assertAll(
                        () -> assertEquals(head, first),
                        () -> assertNotEquals(head.next(), second),
                        () -> assertEquals(head.next(), between)
                    );
                }

                @DisplayName("리턴 값의 다음 노드는 두번째 삽입한 노드와 같다.")
                @Test
                void it_returns_same_second_add_node() {
                    assertEquals(second, between.next());
                }
            }
        }
    }

    @DisplayName("remove 메서드는")
    @Nested
    class Describe_remove {

        @DisplayName("잘못된 position이 주어졌다면")
        @Nested
        class Context_with_invalid_position {
            final int FIRST_NODE_VALUE = 10;
            final int INVALID_POSITION = 1;

            @DisplayName("예외를 던진다.")
            @ParameterizedTest
            @ValueSource(ints = {1, -1, 5})
            void it_throws_IndexOutOfBoundsException() {
                LinkedList linkedList = new LinkedList();
                linkedList.add(new ListNode(FIRST_NODE_VALUE), 0);

                assertThrows(IndexOutOfBoundsException.class,
                    () -> linkedList.remove(INVALID_POSITION));
            }
        }

        @DisplayName("유효한 position이 주어졌다면")
        @Nested
        class Context_with_valid_position {
            final int FIRST_NODE_VALUE = 10;
            final int SECOND_NODE_VALUE = 5;
            final int THIRD_NODE_VALUE = 3;
            final int FIRST_POSITION = 0;
            final int SECOND_POSITION = 1;
            final int THIRD_POSITION = 2;

            @DisplayName("두 개의 노드가 존재하고 맨 앞을 삭제했을 경우")
            @TestInstance(TestInstance.Lifecycle.PER_CLASS)
            @Nested
            class Sub_context_with_front_remove {
                ListNode first, second, removed, head;

                @BeforeAll
                void prepare_remove_front_node() throws NoSuchFieldException, IllegalAccessException {
                    LinkedList linkedList = new LinkedList();
                    first = linkedList.add(new ListNode(FIRST_NODE_VALUE), FIRST_POSITION);
                    second = linkedList.add(new ListNode(SECOND_NODE_VALUE), SECOND_POSITION);
                    removed = linkedList.remove(FIRST_POSITION);

                    Field headField = linkedList.getClass().getDeclaredField("head");
                    headField.setAccessible(true);
                    head = (ListNode)headField.get(linkedList);
                }

                @DisplayName("리턴 값은 첫번째 노드와 같다.")
                @Test
                void it_returns_same_first_add_node() {
                    assertEquals(first, removed);
                }

                @DisplayName("두 번째 삽입한 노드와 head와 같다.")
                @Test
                void it_second_add_node_same_head() {
                    assertEquals(head, second);
                }
            }

            @DisplayName("세 개의 노드가 존재하고 중간을 삭제했을 경우")
            @TestInstance(TestInstance.Lifecycle.PER_CLASS)
            @Nested
            class sub_context_with_between_remove {
                ListNode first, second, third, removed;

                @BeforeAll
                void prepare_remove_between_node() throws NoSuchFieldException, IllegalAccessException {
                    LinkedList linkedList = new LinkedList();
                    first = linkedList.add(new ListNode(FIRST_NODE_VALUE), FIRST_POSITION);
                    second = linkedList.add(new ListNode(SECOND_NODE_VALUE), SECOND_POSITION);
                    third = linkedList.add(new ListNode(THIRD_NODE_VALUE), THIRD_POSITION);
                    removed = linkedList.remove(SECOND_POSITION);
                }

                @DisplayName("리턴 값은 두번째 삽입 노드와 같다.")
                @Test
                void it_returns_same_second_add_node() {
                    assertEquals(second, removed);
                }

                @DisplayName("리턴 값은 다음 노드를 가지지 않는다.")
                @Test
                void it_returns_remove_next_node() {
                    assertNull(removed.next());
                }

                @DisplayName("첫 번째 삽입 노드의 다음 노드는 세번째 삽입한 노드와 같다.")
                @Test
                void it_same_first_node_next_third_add_node() {
                    assertAll(
                        () -> assertEquals(third, first.next()),
                        () -> assertNotEquals(second, first.next())
                    );
                }
            }
        }

    }

    @DisplayName("contains 메서드는")
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    @Nested
    class Describe_contains {
        final int FIRST_NODE_VALUE = 10;
        final int SECOND_NODE_VALUE = 5;
        final int THIRD_NODE_VALUE = 3;
        final int NOT_CONTAINS_NODE_VALUE = -1;
        final int FIRST_POSITION = 0;
        final int SECOND_POSITION = 1;
        final int THIRD_POSITION = 2;

        LinkedList linkedList;
        ListNode first, second, third, contains, notContains;

        @BeforeAll
        void prepare_contains_test() {
            linkedList = new LinkedList();
            first = linkedList.add(new ListNode(FIRST_NODE_VALUE), FIRST_POSITION);
            second = linkedList.add(new ListNode(SECOND_NODE_VALUE), SECOND_POSITION);
            third = linkedList.add(new ListNode(THIRD_NODE_VALUE), THIRD_POSITION);
            contains = first;
            notContains = new ListNode(NOT_CONTAINS_NODE_VALUE);
        }

        @DisplayName("연결 리스트에 존재하는 노드가 주어질 경우")
        @Nested
        class Context_with_contains {
            @DisplayName("true를 리턴한다.")
            @Test
            void it_returns_true() {
                assertTrue(linkedList.contains(contains));
            }
        }

        @Nested
        @DisplayName("연결 리스트에 존재하지 않는 노드가 주어질 경우")
        class Context_with_not_contains {
            @DisplayName("false를 리턴한다.")
            @Test
            void it_returns_false() {
                assertFalse(linkedList.contains(notContains));
            }
        }
    }
}

15

16

17

배운 점

  • 이때까지 테스트를 진행할 땐 Given - When - Then패턴을 많이 사용했다.

  • 이전에 private메서드를 테스트할 때 Java의 Reflection을 사용하여 private 메서드를 테스트해본 적이 있다.

    • 메서드 뿐만 아니라 변수까지도 접근할 수 있는 방법을 알게 되어 사용해보았다.
    Field headField = linkedList.getClass().getDeclaredField("head");
    headField.setAccessible(true);
    ListNode head = (ListNode)headField.get(linkedList);

과제 3. Stack을 구현하세요.

구현

ArrayStack.java
public class ArrayStack {
    private static final int DEFAULT_CAPACITY = 10;
    private static final int[] EMPTY_ARRAY = {};

    private int[] array;
    private int size;

    public ArrayStack() {
        this.array = new int[DEFAULT_CAPACITY];
        this.size = 0;
    }

    public ArrayStack(int capacity) {
        this.array = new int[capacity];
        this.size = 0;
    }

    public void push(int data) {
        if (this.size == array.length) {
            resize();
        }

        array[this.size] = data;
        size++;
    }

    public int pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        int data = array[size - 1];
        size--;
        resize();

        return data;
    }

    private void resize() {
        if (Arrays.equals(array, EMPTY_ARRAY)) {
            array = new int[DEFAULT_CAPACITY];
            return;
        }

        int arrayCapacity = array.length;

        if (size == arrayCapacity) {
            int newCapacity = arrayCapacity * 2;

            array = Arrays.copyOf(array, newCapacity);
            return;
        }

        if (size < (arrayCapacity / 2)) {
            int newCapacity = arrayCapacity / 2;

            array = Arrays.copyOf(array, Math.max(DEFAULT_CAPACITY, newCapacity));
        }
    }
}

테스트

ArrayStackTest.java
@DisplayName("Stack 테스트")
class ArrayStackTest {

    @DisplayName("생성자는")
    @Nested
    class Describe_constructor {
        private int getArraySize(ArrayStack stack) throws IllegalAccessException, NoSuchFieldException {
            Field array = stack.getClass().getDeclaredField("array");
            array.setAccessible(true);
            return ((int[])array.get(stack)).length;
        }

        @DisplayName("아무 인자를 주지 않았다면")
        @Nested
        class Context_with_no_param {
            @DisplayName("크기가 10인 스택이 생성된다")
            @Test
            void it_created_stack_size_ten() throws NoSuchFieldException, IllegalAccessException {
                final int DEFAULT_CAPACITY = 10;

                ArrayStack stack = new ArrayStack();
                int arrayCapacity = getArraySize(stack);
                assertEquals(DEFAULT_CAPACITY, arrayCapacity);
            }
        }

        @DisplayName("인자를 주었다면")
        @Nested
        class Context_with_param {
            @DisplayName("준 인자 크기의 스택이 생성된다")
            @Test
            void it_created_stack_size_give_param() throws NoSuchFieldException, IllegalAccessException {
                final int CAPACITY = 20;

                ArrayStack stack = new ArrayStack(20);
                int arrayCapacity = getArraySize(stack);
                assertEquals(CAPACITY, arrayCapacity);
            }
        }
    }

    @DisplayName("push 메서드는")
    @Nested
    class Describe_push {

        @DisplayName("3개의 데이터가 들어갔을 때")
        @TestInstance(TestInstance.Lifecycle.PER_CLASS)
        @Nested
        class Context_with_three_data_push {
            final int FIRST_DATA = 10;
            final int SECOND_DATA = 20;
            final int THIRD_DATA = 30;
            final int STACK_SIZE = 3;

            ArrayStack stack;

            @BeforeAll
            void setup() {
                stack = new ArrayStack();
                stack.push(FIRST_DATA);
                stack.push(SECOND_DATA);
                stack.push(THIRD_DATA);
            }

            @DisplayName("최상위 데이터는 세번째 데이터와 같다")
            @Test
            void it_top_data_same_third_data() throws IllegalAccessException, NoSuchFieldException {
                Field array = stack.getClass().getDeclaredField("array");
                array.setAccessible(true);
                Field size = stack.getClass().getDeclaredField("size");
                size.setAccessible(true);

                int topData = ((int[])array.get(stack))[(int)size.get(stack) - 1];
                assertEquals(THIRD_DATA, topData);
            }

            @DisplayName("스택 크기는 3이다.")
            @Test
            void it_stack_size_three() throws IllegalAccessException, NoSuchFieldException {
                Field size = stack.getClass().getDeclaredField("size");
                size.setAccessible(true);

                int stackSize = (int)size.get(stack);
                assertEquals(STACK_SIZE, stackSize);
            }
        }

        @DisplayName("스택이 꽉 찬 상태라면")
        @Nested
        class Context_with_stack_full {
            final int CAPCITY = 2;
            ArrayStack stack;

            @DisplayName("새로운 용적을 현재 용적의 2배로 설정한다.")
            @Test
            void it_capacity_double() throws IllegalAccessException, NoSuchFieldException {
                stack = new ArrayStack(CAPCITY);
                for (int i = 0; i < 3; i++) {
                    stack.push(i);
                }

                Field array = stack.getClass().getDeclaredField("array");
                array.setAccessible(true);
                int arraySize = ((int[])array.get(stack)).length;
                assertEquals(CAPCITY * 2, arraySize);
            }
        }

    }

    @DisplayName("pop 메서드는")
    @Nested
    class Describe_pop {

        @DisplayName("스택이 비워져 있을 때")
        @Nested
        class Context_with_empty_stack {
            @DisplayName("예외를 발생한다.")
            @Test
            void it_thorws_exception() {
                ArrayStack stack = new ArrayStack();
                assertThrows(EmptyStackException.class, stack::pop);
            }
        }

        @DisplayName("3개의 데이터가 존재할 경우")
        @Nested
        class Context_with_exist_three_data {
            final int FIRST_DATA = 10;
            final int SECOND_DATA = 20;
            final int THIRD_DATA = 30;
            ArrayStack stack;

            @BeforeEach
            void setup() {
                stack = new ArrayStack();
                stack.push(FIRST_DATA);
                stack.push(SECOND_DATA);
                stack.push(THIRD_DATA);
            }

            @DisplayName("리턴 값과 마지막으로 넣은 데이터가 같다.")
            @Test
            void it_returns_same_last_push_data() {
                assertEquals(THIRD_DATA, stack.pop());
            }

            @DisplayName("최상위 데이터는 두번째로 넣은 값과 같다.")
            @Test
            void it_top_data_same_second_data() throws IllegalAccessException, NoSuchFieldException {
                stack.pop();
                Field array = stack.getClass().getDeclaredField("array");
                array.setAccessible(true);
                Field size = stack.getClass().getDeclaredField("size");
                size.setAccessible(true);

                int topData = ((int[])array.get(stack))[(int)size.get(stack) - 1];
                assertEquals(SECOND_DATA, topData);
            }
        }

        @DisplayName("스택이 용적의 반보다 작아질 때")
        @Nested
        class Context_with_stack_size_half {
            final int OVER_CAPACITY = 30;
            final int NOT_OVER_CAPACITY = 18;
            final int DEFAULT_CAPACITY = 10;

            ArrayStack stack;

            @DisplayName("용적의 반이 기본 용적보다 클 경우")
            @Nested
            class SubContext_with_stack_size_half_over_default_size {
                @DisplayName("새로운 용적을 용적의 반으로 설정한다.")
                @Test
                void it_capcity_size_half() throws IllegalAccessException, NoSuchFieldException {
                    stack = new ArrayStack(OVER_CAPACITY);
                    for (int i = 0; i < 15; i++) {
                        stack.push(i);
                    }
                    stack.pop();

                    Field array = stack.getClass().getDeclaredField("array");
                    array.setAccessible(true);

                    int arraySize = ((int[])array.get(stack)).length;
                    assertEquals(OVER_CAPACITY / 2, arraySize);
                }
            }

            @DisplayName("용적의 반이 기본 용적보다 작을 경우")
            @Nested
            class SubContext_with_stack_size_half_not_over_default_size {
                @DisplayName("새로운 용적을 기본 용적으로 설정한다.")
                @Test
                void it_capcity_default() throws IllegalAccessException, NoSuchFieldException {
                    stack = new ArrayStack(NOT_OVER_CAPACITY);
                    for (int i = 0; i < 9; i++) {
                        stack.push(i);
                    }
                    stack.pop();

                    Field array = stack.getClass().getDeclaredField("array");
                    array.setAccessible(true);

                    int arraySize = ((int[])array.get(stack)).length;
                    assertEquals(DEFAULT_CAPACITY, arraySize);
                }
            }
        }
    }
}

18

과제 4. 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.

구현

ListNodeStack.java
public class ListNodeStack {
    private ListNode head;
    private int size;

    public ListNodeStack() {
        this.head = null;
        this.size = 0;
    }

    public void push(int data) {
        if (head == null) {
            head = new ListNode(data);
            size++;
            return;
        }

        ListNode lastNode = head;

        while (lastNode.next() != null) {
            lastNode = lastNode.next();
        }

        lastNode.linking(new ListNode(data));
        size++;
    }

    public int pop() {
        if (head == null) {
            throw new EmptyStackException();
        }

        ListNode beforeNode = null;
        ListNode lastNode = head;

        while (lastNode.next() != null) {
            beforeNode = lastNode;
            lastNode = lastNode.next();
        }

        int data = lastNode.getData();

        if (beforeNode == null) {
            head = null;
        } else {
            beforeNode.linking(null);
        }

        size--;
        return data;
    }
}
ListNodeStackTest.java

테스트

@DisplayName("ListNodeStack 테스트")
class ListNodeStackTest {

    @DisplayName("push 메서드는")
    @Nested
    class Describe_push {

        @DisplayName("3개의 데이터가 들어갔을 때")
        @Nested
        class Context_with_three_data_push {
            final int FIRST_DATA = 10;
            final int SECOND_DATA = 20;
            final int THIRD_DATA = 30;
            final int STACK_SIZE = 3;

            ListNodeStack stack;

            @BeforeEach
            void setup() {
                stack = new ListNodeStack();
                stack.push(FIRST_DATA);
                stack.push(SECOND_DATA);
                stack.push(THIRD_DATA);
            }

            @DisplayName("최상위 데이터는 세번째 데이터와 같다")
            @Test
            void it_top_data_same_third_data() throws IllegalAccessException, NoSuchFieldException {
                assertEquals(THIRD_DATA, stack.pop());
            }

            @DisplayName("스택 크기는 3이다.")
            @Test
            void it_stack_size_three() throws IllegalAccessException, NoSuchFieldException {
                Field size = stack.getClass().getDeclaredField("size");
                size.setAccessible(true);

                int stackSize = (int)size.get(stack);
                assertEquals(STACK_SIZE, stackSize);
            }
        }

        @DisplayName("1개의 데이터가 들어갔을 때")
        @Nested
        class Context_with_one_data_push {
            final int FIRST_DATA = 10;
            final int STACK_SIZE = 1;
            ListNodeStack stack;

            @DisplayName("스택 크기는 1이다.")
            @Test
            void it_stack_size_one() throws IllegalAccessException, NoSuchFieldException {
                Field size = stack.getClass().getDeclaredField("size");
                size.setAccessible(true);

                int stackSize = (int)size.get(stack);
                assertEquals(STACK_SIZE, stackSize);
            }

            @BeforeEach
            void setup() {
                stack = new ListNodeStack();
                stack.push(FIRST_DATA);
            }

            @DisplayName("최상위 데이터는 head와 같다.")
            @Test
            void it_top_data_same_head() throws NoSuchFieldException, IllegalAccessException {
                Field head = stack.getClass().getDeclaredField("head");
                head.setAccessible(true);
                assertEquals(FIRST_DATA, ((ListNode)head.get(stack)).getData());
            }
        }
    }

    @DisplayName("pop 메서드는")
    @Nested
    class Describe_pop {

        @DisplayName("스택이 비워져 있을 때")
        @Nested
        class Context_with_empty_stack {
            @DisplayName("예외를 발생한다.")
            @Test
            void it_thorws_exception() {
                ListNodeStack stack = new ListNodeStack();
                assertThrows(EmptyStackException.class, stack::pop);
            }
        }

        @DisplayName("3개의 데이터가 존재할 경우")
        @Nested
        class Context_with_exist_three_data {
            final int FIRST_DATA = 10;
            final int SECOND_DATA = 20;
            final int THIRD_DATA = 30;
            ListNodeStack stack;

            @BeforeEach
            void setup() {
                stack = new ListNodeStack();
                stack.push(FIRST_DATA);
                stack.push(SECOND_DATA);
                stack.push(THIRD_DATA);
            }

            @DisplayName("리턴 값과 마지막으로 넣은 데이터가 같다.")
            @Test
            void it_returns_same_last_push_data() {
                assertEquals(THIRD_DATA, stack.pop());
            }

            @DisplayName("최상위 데이터는 두번째로 넣은 값과 같다.")
            @Test
            void it_top_data_same_second_data() throws IllegalAccessException, NoSuchFieldException {
                stack.pop();
                assertEquals(SECOND_DATA, stack.pop());
            }
        }
    }
}

19

과제 5. Queue를 구현하세요.

배열 기반 Queue

구현

ArrayQueue.java
public class ArrayQueue {
    private static final int DEFAULT_CAPACITY = 64;
    private int[] array;
    private int size;
    private int front;
    private int rear;

    public ArrayQueue() {
        this.array = new int[DEFAULT_CAPACITY];
        this.size = 0;
        this.front = 0;
        this.rear = 0;
    }

    public ArrayQueue(int capacity) {
        this.array = new int[capacity];
        this.size = 0;
        this.front = 0;
        this.rear = 0;
    }

    private void resize(int newCapacity) {
        int arrayCapacity = array.length;
        int[] newArray = new int[newCapacity];

        for (int i = 1, j = front + 1; i <= size; i++, j++) {
            newArray[i] = array[j % arrayCapacity];
        }

        this.array = newArray;

        front = 0;
        rear = size;
    }

    public boolean offer(int data) {

        if (isFull()) {
            resize(array.length * 2);
        }

        rear = (rear + 1) % array.length;

        array[rear] = data;
        size++;

        return true;
    }

    private boolean isFull() {
        return (rear + 1) % array.length == front;
    }

    //본래 add는 용적이 제한되어있는 데 용적보다 많은 요소를 추가할 경우 예외를 던지지만
    //여기서는 최대 제한을 걸지 않았기 때문에 넘어간다.
    public boolean add(int data) {
        return offer(data);
    }

    public Integer poll() {
        if (isEmpty()) {
            return null;
        }

        front = (front + 1) % array.length;

        int data = array[front];
        size--;

        if (array.length > DEFAULT_CAPACITY && size < (array.length / 4)) {
            resize(Math.max(DEFAULT_CAPACITY, array.length / 2));
        }

        return data;
    }

    public int remove() {
        Integer data = poll();

        if (data == null) {
            throw new NoSuchElementException();
        }

        return data;
    }

    public Integer peek() {
        if (isEmpty()) {
            return null;
        }

        int data = array[(front + 1) % array.length];
        return data;
    }

    public int element() {
        Integer data = peek();

        if (data == null) {
            throw new NoSuchElementException();
        }

        return data;
    }

    private boolean isEmpty() {
        return size == 0;
    }
}

테스트

ArrayQueueTest.java
@DisplayName("ArrayQueue 테스트")
class ArrayQueueTest {

    @DisplayName("생성자는")
    @Nested
    class Describe_constructor {
        private int getArraySize(ArrayQueue queue) throws IllegalAccessException, NoSuchFieldException {
            Field array = queue.getClass().getDeclaredField("array");
            array.setAccessible(true);
            return ((int[])array.get(queue)).length;
        }

        @DisplayName("아무 인자를 주지 않았다면")
        @Nested
        class Context_with_no_param {
            @DisplayName("크기가 64인 큐가 생성된다")
            @Test
            void it_created_queue_size_sixty_four() throws NoSuchFieldException, IllegalAccessException {
                final int DEFAULT_CAPACITY = 64;

                ArrayQueue queue = new ArrayQueue();
                int arrayCapacity = getArraySize(queue);
                assertEquals(DEFAULT_CAPACITY, arrayCapacity);
            }
        }

        @DisplayName("인자를 주었다면")
        @Nested
        class Context_with_param {
            @DisplayName("준 인자 크기의 큐가 생성된다")
            @Test
            void it_created_stack_size_give_param() throws NoSuchFieldException, IllegalAccessException {
                final int CAPACITY = 20;

                ArrayQueue queue = new ArrayQueue(20);
                int arrayCapacity = getArraySize(queue);
                assertEquals(CAPACITY, arrayCapacity);
            }
        }
    }

    @DisplayName("offer 메서드는")
    @Nested
    class Describe_offer {
        @DisplayName("여러개의 데이터를 삽입한다면")
        @Nested
        class Context_with_insert_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;
            final int INITIAL_INDEX = 0;
            ArrayQueue queue;

            @BeforeEach
            void setup() {
                queue = new ArrayQueue();
            }

            private int getFieldFromQueue(String str) throws NoSuchFieldException, IllegalAccessException {
                Field field = queue.getClass().getDeclaredField(str);
                field.setAccessible(true);
                return (int)field.get(queue);
            }

            @DisplayName("각각 참을 반환한다.")
            @Test
            void it_returns_true() {
                assertAll(
                    () -> assertTrue(queue.offer(FIRST_DATA_VALUE)),
                    () -> assertTrue(queue.offer(SECOND_DATA_VALUE)),
                    () -> assertTrue(queue.offer(THIRD_DATA_VALUE))
                );
            }

            @DisplayName("rear 필드만 1씩 증가한다.")
            @Test
            void it_increased_rear() {
                assertAll(
                    () -> {
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_INDEX, front),
                            () -> assertEquals(INITIAL_INDEX, rear)
                        );
                    }
                );

                assertAll(
                    () -> {
                        queue.add(FIRST_DATA_VALUE);
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_INDEX, front),
                            () -> assertEquals(INITIAL_INDEX + 1, rear)
                        );
                    }
                );

                assertAll(
                    () -> {
                        queue.add(SECOND_DATA_VALUE);
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_INDEX, front),
                            () -> assertEquals(INITIAL_INDEX + 2, rear)
                        );
                    }
                );
            }
        }

        @DisplayName("큐가 꽉 찬 상태라면")
        @Nested
        class Context_with_queue_full {
            final int CAPCITY = 2;
            ArrayQueue queue;

            @DisplayName("새로운 용적을 현재 용적의 2배로 설정한다.")
            @Test
            void it_capacity_double() throws IllegalAccessException, NoSuchFieldException {
                queue = new ArrayQueue(CAPCITY);
                for (int i = 0; i < 3; i++) {
                    queue.offer(i);
                }

                Field array = queue.getClass().getDeclaredField("array");
                array.setAccessible(true);
                int arraySize = ((int[])array.get(queue)).length;
                assertEquals(CAPCITY * 2, arraySize);
            }
        }

    }

    @DisplayName("poll 메서드는")
    @Nested
    class Describe_poll {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("null을 반환한다.")
            @Test
            void it_returns_null() {
                ArrayQueue queue = new ArrayQueue();
                assertNull(queue.poll());
            }
        }

        @DisplayName("여러개의 데이터가 존재할 때")
        @Nested
        class Context_with_exist_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;
            final int INITIAL_FRONT = 0;
            final int INITIAL_REAR = 3;
            ArrayQueue queue;

            @BeforeEach
            void setup() {
                queue = new ArrayQueue();
                queue.offer(FIRST_DATA_VALUE);
                queue.offer(SECOND_DATA_VALUE);
                queue.offer(THIRD_DATA_VALUE);
            }

            private int getFieldFromQueue(String str) throws NoSuchFieldException, IllegalAccessException {
                Field field = queue.getClass().getDeclaredField(str);
                field.setAccessible(true);
                return (int)field.get(queue);
            }

            @DisplayName("가장 앞에 있는 데이터를 반환한다.")
            @Test
            void it_returns_first_data() {
                assertAll(
                    () -> assertEquals(FIRST_DATA_VALUE, queue.poll()),
                    () -> assertEquals(SECOND_DATA_VALUE, queue.poll())
                );
            }

            @DisplayName("front 필드만 1씩 증가한다.")
            void it_increased_front() {
                assertAll(
                    () -> {
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_FRONT, front),
                            () -> assertEquals(INITIAL_REAR, rear)
                        );
                    }
                );

                assertAll(
                    () -> {
                        queue.add(FIRST_DATA_VALUE);
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_FRONT + 1, front),
                            () -> assertEquals(INITIAL_REAR, rear)
                        );
                    }
                );

                assertAll(
                    () -> {
                        queue.add(FIRST_DATA_VALUE);
                        int front = getFieldFromQueue("front");
                        int rear = getFieldFromQueue("rear");
                        assertAll(
                            () -> assertEquals(INITIAL_FRONT + 2, front),
                            () -> assertEquals(INITIAL_REAR, rear)
                        );
                    }
                );
            }
        }
    }

    @DisplayName("remove 메서드는")
    @Nested
    class Describe_remove {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("예외를 반환한다.")
            @Test
            void it_throws_exception() {
                ArrayQueue queue = new ArrayQueue();
                assertThrows(NoSuchElementException.class, queue::remove);
            }
        }
    }

    @DisplayName("peek 메서드는")
    @Nested
    class Describe_peek {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("null을 반환한다.")
            @Test
            void it_returns_null() {
                ArrayQueue queue = new ArrayQueue();
                assertNull(queue.peek());
            }
        }

        @DisplayName("여러 개의 데이터가 존재할 때")
        @Nested
        class Context_with_exist_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;

            @DisplayName("가장 앞에 있는 데이터를 반환한다.")
            @Test
            void it_returns_front_data() {
                ArrayQueue queue = new ArrayQueue();
                assertAll(
                    () -> {
                        queue.add(FIRST_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.add(SECOND_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.add(THIRD_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.poll();
                        assertEquals(SECOND_DATA_VALUE, queue.peek());
                    }
                );
            }

        }

    }

    @DisplayName("element 메서드는")
    @Nested
    class Describe_element {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("예외를 던진다.")
            @Test
            void it_throws_exception() {
                ArrayQueue queue = new ArrayQueue();
                assertThrows(NoSuchElementException.class, queue::element);
            }
        }
    }
}

20

21

ListNode 기반 Queue

구현

ListNodeQueue.java
public class ListNodeQueue {
    private ListNode front;
    private ListNode rear;
    private int size;

    public ListNodeQueue() {
        this.front = null;
        this.rear = null;
        this.size = 0;
    }

    public boolean offer(int data) {
        ListNode offerNode = new ListNode(data);

        if (isEmpty()) {
            front = offerNode;
        } else {
            rear.linking(offerNode);
        }

        rear = offerNode;
        size++;
        return true;
    }

    public boolean add(int data) {
        return offer(data);
    }

    public Integer poll() {
        if (isEmpty()) {
            return null;
        }

        int data = front.getData();
        front = front.next();
        size--;
        return data;
    }

    public int remove() {
        Integer data = poll();

        if (data == null) {
            throw new NoSuchElementException();
        }

        return data;
    }

    public Integer peek() {
        if (isEmpty()) {
            return null;
        }

        return front.getData();
    }

    public int element() {
        Integer data = peek();

        if (data == null) {
            throw new NoSuchElementException();
        }

        return data;
    }

    private boolean isEmpty() {
        return size == 0;
    }
}

테스트

ListNodeQueueTest.java
@DisplayName("ListNodeQueue 테스트")
class ListNodeQueueTest {

    @DisplayName("offer 메서드는")
    @Nested
    class Describe_offer {
        @DisplayName("여러개의 데이터를 삽입한다면")
        @Nested
        class Context_with_insert_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;
            final int INITIAL_INDEX = 0;
            ListNodeQueue queue;

            @BeforeEach
            void setup() {
                queue = new ListNodeQueue();
            }

            private ListNode getFieldFromQueue(String str) throws NoSuchFieldException, IllegalAccessException {
                Field field = queue.getClass().getDeclaredField(str);
                field.setAccessible(true);
                return (ListNode)field.get(queue);
            }

            @DisplayName("각각 참을 반환한다.")
            @Test
            void it_returns_true() {
                assertAll(
                    () -> assertTrue(queue.offer(FIRST_DATA_VALUE)),
                    () -> assertTrue(queue.offer(SECOND_DATA_VALUE)),
                    () -> assertTrue(queue.offer(THIRD_DATA_VALUE))
                );
            }

            @DisplayName("rear 노드만 변경된다.")
            void it_changed_only_rear() {
                assertAll(
                    () -> compareBeforeAfterToAdd(FIRST_DATA_VALUE),
                    () -> compareBeforeAfterToAdd(SECOND_DATA_VALUE),
                    () -> compareBeforeAfterToAdd(THIRD_DATA_VALUE)
                );
            }

            private void compareBeforeAfterToAdd(int data) throws NoSuchFieldException, IllegalAccessException {
                ListNode beforeFront = getFieldFromQueue("front");
                ListNode beforeRear = getFieldFromQueue("rear");
                queue.add(data);
                ListNode afterFront = getFieldFromQueue("front");
                ListNode afterRear = getFieldFromQueue("rear");
                assertAll(
                    () -> assertEquals(beforeFront, afterFront),
                    () -> assertNotEquals(beforeRear, afterRear)
                );
            }
        }
    }

    @DisplayName("poll 메서드는")
    @Nested
    class Describe_poll {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("null을 반환한다.")
            @Test
            void it_returns_null() {
                ListNodeQueue queue = new ListNodeQueue();
                assertNull(queue.poll());
            }
        }

        @DisplayName("여러개의 데이터가 존재할 때")
        @Nested
        class Context_with_exist_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;
            final int INITIAL_FRONT = 0;
            final int INITIAL_REAR = 3;
            ListNodeQueue queue;

            @BeforeEach
            void setup() {
                queue = new ListNodeQueue();
                queue.offer(FIRST_DATA_VALUE);
                queue.offer(SECOND_DATA_VALUE);
                queue.offer(THIRD_DATA_VALUE);
            }

            private ListNode getFieldFromQueue(String str) throws NoSuchFieldException, IllegalAccessException {
                Field field = queue.getClass().getDeclaredField(str);
                field.setAccessible(true);
                return (ListNode)field.get(queue);
            }

            @DisplayName("가장 앞에 있는 데이터를 반환한다.")
            @Test
            void it_returns_first_data() {
                assertAll(
                    () -> assertEquals(FIRST_DATA_VALUE, queue.poll()),
                    () -> assertEquals(SECOND_DATA_VALUE, queue.poll())
                );
            }

            @DisplayName("front 노드만 변경된다.")
            @Test
            void it_changed_only_front() {
                assertAll(
                    this::compareBeforeAfterToAdd,
                    this::compareBeforeAfterToAdd,
                    this::compareBeforeAfterToAdd
                );
            }

            private void compareBeforeAfterToAdd() throws NoSuchFieldException, IllegalAccessException {
                ListNode beforeFront = getFieldFromQueue("front");
                ListNode beforeRear = getFieldFromQueue("rear");
                queue.poll();
                ListNode afterFront = getFieldFromQueue("front");
                ListNode afterRear = getFieldFromQueue("rear");
                assertAll(
                    () -> assertNotEquals(beforeFront, afterFront),
                    () -> assertEquals(beforeRear, afterRear)
                );
            }
        }
    }

    @DisplayName("remove 메서드는")
    @Nested
    class Describe_remove {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("예외를 반환한다.")
            @Test
            void it_throws_exception() {
                ListNodeQueue queue = new ListNodeQueue();
                assertThrows(NoSuchElementException.class, queue::remove);
            }
        }
    }

    @DisplayName("peek 메서드는")
    @Nested
    class Describe_peek {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("null을 반환한다.")
            @Test
            void it_returns_null() {
                ListNodeQueue queue = new ListNodeQueue();
                assertNull(queue.peek());
            }
        }

        @DisplayName("여러 개의 데이터가 존재할 때")
        @Nested
        class Context_with_exist_data {
            final int FIRST_DATA_VALUE = 5;
            final int SECOND_DATA_VALUE = 4;
            final int THIRD_DATA_VALUE = 3;

            @DisplayName("가장 앞에 있는 데이터를 반환한다.")
            @Test
            void it_returns_front_data() {
                ListNodeQueue queue = new ListNodeQueue();
                assertAll(
                    () -> {
                        queue.add(FIRST_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.add(SECOND_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.add(THIRD_DATA_VALUE);
                        assertEquals(FIRST_DATA_VALUE, queue.peek());
                    },

                    () -> {
                        queue.poll();
                        assertEquals(SECOND_DATA_VALUE, queue.peek());
                    }
                );
            }
        }
    }

    @DisplayName("element 메서드는")
    @Nested
    class Describe_element {
        @DisplayName("큐가 비어있는 상태라면")
        @Nested
        class Context_with_queue_empty {
            @DisplayName("예외를 던진다.")
            @Test
            void it_throws_exception() {
                ListNodeQueue queue = new ListNodeQueue();
                assertThrows(NoSuchElementException.class, queue::element);
            }
        }
    }
}

22

Reference

댓글