[Spring] Spring Security 개요

[Spring] Spring Security 개요

#목차

Spring Security 개요

  • 해당 포스트에서는 Spring Security의 핵심적인 인증(Authentication), 인가(Authorization), 보안과 관련된 개념을 다룬다.

Spring Security 란?

Spring Security는 Java 웹 어플리케이션의 인증인가(권한 부여) 를 제공하며, 웹 공격에 대한 방어 기능을 제공 해주는 스프링의 하위 프레임워크이다. - spring.io

인증과 권한(인가)이란?

  • 인증과 권한(인가)에 대한 자세한 내용은 아래 링크를 통해 확인 할 수 있다.

Continue with [Security] 인증과 인가

간단히 인증과 인가(권한 부여)에 대한 뜻을 알아보자.

  • 인증 - Authentication
    • (지문 또는 얼굴 등) 입증 가능한 정보로 사용자의 신원을 확인하는 과정이다.
    • 즉, 식별 가능한 정보로 서비스에 등록된 유저의 신원을 입증하는 과정
  • 인가(권한 부여) - Authorization
    • 입증(인증) 된 신원권한에 대한 허가를 나타내는 것이 인가이다.
    • 즉, 인증된 사용자에 대한 특정 리소스에 접근할 권한이 있는지 확인하는 과정

간단한 표로 인증과 인가의 차이를 알아보자.

-인증(Authentication)인가(Authorization)
기능사용자에 대한 자격 증명 확인사용자에 대한 권한 부여 또는 거부
작동비밀번호, 생체 인식, 일회용 핀 또는 앱을 통해 작동개발,보안팀에서 관리하는 설정을 통해
데이터 이동ID 토큰을 통해 데이터 이동액세스 토큰(Access Token)을 통해 데이터 이동

Spring Security에서의 인증, 인가 그리고 보호의 기능

인증(Authentication)

  • 스프링 시큐리티의 인증은 양식((e.g)로그인 양식) 기반 인증, HTTP 기본 인증, 세션 기반 인증, JWT(Json Web Token) 기반 인증, OAuth 2.0, OpenID Connect 등을 비롯한 다양한 인증 메커니즘을 제공한다.

인가(권한 부여(Authorization))

  • 스프링 시큐리티의 인가는 애플리케이션에서 어떤 사용자가 특정 리소스에 액세스(접근) 할 수 있는지 세밀 하게 제어할 수 있다.
  • 이는 역할 기반 액세스 제어(RBAC), 표현식 기반 액세스 제어(EL 기반) 와 같은 다양한 기술을 사용하여 수행할 수 있다.

웹 공격으로부터의 보호

  • 사이트 간 스크립팅(XSS), 사이트 간 요청 위조(CSRF), 세션 고정과 같은 웹 사이트 공격에 대한 기본 보호 기능을 제공한다.

Spring Security의 핵심 역할

인증(Authentication) : 사용자의 신원을 확인하는 프로세스

  • 양식((e.g)로그인 양식) 기반 인증
    • 가장 일반적인 인증 유형으로, 사용자가 로그인 양식에 사용자 이름(username)과 비밀번호(password)를 입력하는 방식이다.
  • HTTP 기본 인증
    • 이 인증 유형은 사용자가 모든 요청(Request)HTTP 헤더에 자격 증명을 제공해야 한다.
    • 웹 서버와 클라이언트 간의 간단한 인증 방식이며, 이 방식은 HTTP 프로토콜내장된 메커니즘으로, 사용자 이름과 비밀번호를 Base64로 인코딩한 후 HTTP 헤더에 추가하여 서버에 전송하게된다.
  • OAuth 2.0
    • OAuth 2.0은 인증에 널리 사용되는 개방형 표준이다.
    • Spring Security는 OAuth 2.0을 지원하므로 애플리케이션을 다른 OAuth 2.0 호환 시스템과 쉽게 통합할 수 있다.
      • e.g) Google, Naver, Kakao 로그인 등등…

Continue with [Security] OAuth 2.0

  • OpenID Connect
    • OpenID ConnectOAuth 2.0를 기반으로 구축된 ID 계층이다.
    • 사용자 프로필 정보 및 싱글 사인온(SSO)과 같은 추가 기능을 제공한다.

      SSO(Single Sign On) 이란?
      한 번의 로그인으로 여러 가지 다른 사이트들을 자동적으로 접속하여 이용하는 방법을 말한다.

Continue with [Security] OpenID Connect

인가(권한 부여(Authorization)): 사용자가 특정 리소스에 액세스(접근)할 수 있는지 여부를 결정하는 프로세스

  • 역할 기반 액세스 제어(RBAC(Role-Based Access Control))
    • 사용자의 역할에 따라 시스템 리소스에 대한 접근을 제어하는 방식이며, 이는 사용자가 아닌 “역할”에 권한을 부여 하고, 사용자는 해당 역할을 통해 권한을 얻게 된다.
    • 예를 들어, ‘관리자’, ‘사용자’, ‘게스트’ 등의 역할미리 정의하고, 이 역할에 따라 특정 리소스에 대한 접근 권한을 부여한다.
    • RBAC는 간단하고 이해하기 쉬운 모델로, 대부분의 시스템에서 널리 사용되고 있습니다.
  • 표현식 기반 액세스 제어(EL(Expression Language) 기반)
    • EL 기반 액세스 제어보안 결정을 위해 보안 표현식을 사용하는 방식이다.
    • 이는 사용자의 역할, 액세스하려는 리소스, 애플리케이션의 현재 상태와 같은 여러 요소에 따라 달라지는 권한 부여 규칙을 구현하는 데 유용할 수 있다.
    • 또한, 사용자의 역할 뿐만 아니라 다양한 조건(로그인 상태, IP 주소, 시간 등)을 사용하여 접근 권한을 판단한다.
    • 예를 들어, .hasRole('ROLE_ADMIN')이나 .hasIpAddress('192.168.1.0/24')와 같은 표현식을 사용할 수 있다.

웹 공격으로부터의 보호

Spring Security는 다양한 웹 공격으로부터 애플리케이션을 보호한다.

  • CSRF(Cross-Site Request Forgery) 공격 방어
    • CSRF(Cross-Site Request Forgery)사용자가 자신의 의지와는 무관하게 공격자가 의도한 행동을 하도록 만드는 공격이다.
    • 스프링 시큐리티CSRF 토큰을 사용하여 이러한 공격을 방어하게 되는데, 기능적으로 보면 CSRF 토큰서버에서 생성하여 클라이언트에게 전달하고, 클라이언트는 이후 요청에 이 토큰을 포함하여 서버로 전송한다.
    • 서버는 이 토큰을 검증하여 유효한 요청인지를 판단한다.
  • 세션 고정(Session Fixation) 공격 방어
    • 세션 고정 공격공격자가 특정 세션 ID를 미리 알아내고 이를 이용하여 다른 사용자의 세션을 탈취하려는 공격이다.
    • 스프링 시큐리티로그인 시 새로운 세션을 생성하여 이전 세션의 정보를 새 세션으로 복사하는 방식으로 세션 고정 공격을 방어한다.
  • 클릭재킹(Clickjacking) 방어
    • 클릭재킹사용자가 의도한 곳이 아니라 다른 곳을 클릭하도록 만드는 공격이다.
    • 스프링 시큐리티X-Frame-Options 헤더 를 이용하여 이러한 공격을 방어한다.
    • 이 헤더를 통해 브라우저는 현재 페이지가 iframe 내에서 렌더링되는 것을 방지할 수 있다.

      iframe이란?
      현재 HTML 페이지 안에 다른 HTML 페이지를 포함시키는 중첩된 브라우저이다.
      iframe 요소를 이용하면 해당 웹 페이지 안에 어떠한 제한 없이 다른 페이지를 불러와서 삽입 할 수 있다.

  • XSS(Cross-Site Scripting) 방어
    • XSS(Cross-Site Scripting)공격자가 웹 페이지에 악성 스크립트를 삽입하여 다른 사용자의 데이터를 탈취하거나 조작하는 공격이다.
    • 스프링 시큐리티사용자의 입력을 필터링하거나 이스케이프하여 이러한 공격을 방어한다.

      이스케이프 처리란?
      & => &amp;, ' => &#x27;, " => &quot;, < => &lt;, > => &gt;, / => &#x2F;
      즉, 코드로써 작성될 수 있는 기호들을 이스케이프 처리한 텍스트로 작성하는 것이다.

  • SQL Injection 방지
    • SQL 인젝션공격자가 애플리케이션의 입력값을 조작하여 의도치 않은 SQL 쿼리를 실행하는 공격이다.
    • 스프링 시큐리티 자체는 SQL 인젝션을 방지하는 기능을 제공하지 않지만, 스프링 프레임워크의 일부인 JdbcTemplate 또는 JPA, Hibernate와 같은 ORM(Object Relational Mapping) 도구를 사용하면 SQL 인젝션을 방지할 수 있다.
    • 이들 도구는 파라미터 바인딩을 통해 쿼리를 안전하게 만들어 준다.

      파라미터 바인딩이란?

      • 이름 기반 바인딩 : =:연산자를 사용, 메서드 체이닝을 사용
        Query query = em.createQuery("select m from Member m where m.username =: username").setParameter("username", userName);    
        
      • 위치 기반 바인딩 : =?연산자를 사용, 메서드 체이닝을 사용
        Query query = em.createQuery("select m from Member m where m.username =? 1").setParameter(1, userName);    
        
  • 권한 우회 공격 방어
    • 권한 우회 공격공격자가 본인의 권한 이외의 리소스에 접근하려는 시도이다.
    • 스프링 시큐리티URL 레벨이나 메소드 레벨에서 접근 제어를 설정하여 이러한 공격을 방어할 수 있다.
    • 예를 들어, 특정 URL은 관리자 권한을 가진 사용자만 접근할 수 있도록 설정할 수 있다.
    • 또한, 표현식 기반 EL(Expression Language)의 접근 제어를 통해 더욱 세밀한 권한 설정이 가능하다.

정리

스프링 시큐리티는 이런 방식으로 인증, 인가 과정을 관리하고, 애플리케이션을 다양한 웹 공격으로부터 보호한다. 이를 통해 개발자는 보안과 관련된 부분에 대해 신경 쓰지 않고, 비즈니스 로직에 집중할 수 있게 해준다.

Reference

[JPA] QueryDSL에서 MySQL group_concat 함수 등록과 사용법

[JPA] QueryDSL에서 MySQL group_concat 함수 등록과 사용법

#목차

MySQL group_concat

  • JPA QueryDSL에서 group_concat() 함수 사용법

MySQL group_concat 함수 란?

  • GROUP_CONCATMySQL에서 제공하는 집계 함수 중 하나로, 선택한 row(열)의 값을 연결하여 하나의 문자열로 반환 한다.
  • 기본적으로 이 함수는 쉼표(,)를 사용하여 값들을 연결하지만, SEPARATOR 키워드를 사용하여 다른 문자 또는 문자열을 구분자로 지정할 수 있다.

MySQL group_concat 기본 사용법

GROUP_CONCAT() 문법

GROUP_CONCAT(
    [DISTINCT] expression [,expression ...]
    [ORDER BY {unsigned_integer | col_name | expr}
    [ASC | DESC] [,col_name ...]]
    [SEPARATOR str_val]
)
  • expression : GROUP_CONCAT 함수가 적용될 열 또는 식.
  • ORDER BY : 결과를 정렬하는 데 사용되며, 필요에 따라 생략 가능.
  • ASC | DESC : 정렬 순서를 지정하며, 필요에 따라 생략할 수 있다
  • SEPARATOR : 값들을 연결할 때 사용할 구분자를 지정. 기본값은 쉼표(,).
SELECT 
    student_name,
    GROUP_CONCAT(
        DISTINCT test_score ORDER BY test_score DESC SEPARATOR ' '
    )
FROM student
GROUP BY student_name;

GROUP_CONCAT() 예제

CREATE TABLE test (
    id INT,
    value VARCHAR(255)
);

INSERT INTO test (id, value) VALUES (1, 'Apple');
INSERT INTO test (id, value) VALUES (1, 'Banana');
INSERT INTO test (id, value) VALUES (2, 'Cherry');
INSERT INTO test (id, value) VALUES (2, 'Date');

SELECT id,
       GROUP_CONCAT(value ORDER BY value ASC SEPARATOR ', ')
FROM test
GROUP BY id;

GROUP_CONCAT() 예제 결과

1  Apple,Banana
2  Cherry,Dates

JPA QueryDSL에서 group_concat() 적용하기

문제 발생

Hibernate 는 특정 데이터베이스에 종속되지 않고 객체지향스럽게 사용할 수 있도록 추상화해주어 특정 DB에 종속된 함수(예, MySQLgroup_concat)는 제공하지 않는다.
때문에 Hibernate에서 제공하지 않고 특정 DB에서 제공하는 함수를 사용하려면 Dialect를 커스텀 하여 SQL 함수를 등록해주어야한다.”

CustomMySQLDialect 파일 생성

  • package com.xxxxx.xxxxx.xxx.config;CustomMySQLDialect.class 파일을 생성
    • 각 프로젝트에서 config 폴더에 생성하면 무난할듯 하다.
package com.xxxxx.xxxxx.xxx.config;

import org.hibernate.dialect.MySQL57Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;

public class CustomMySQLDialect extends MySQL57Dialect {
    public CustomMySQLDialect() {
        super();
        // JPA QueryDSL에서 group_concat 함수를 사용할 수 있도록 등록
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

주의!

  • 위 사진처럼 MySQL5InnoDBDialectMySQL57InnoDBDialectdeprecated(사용하지 않는) 되어있다.
  • 그래서 난 위 코드처럼 MySQL57Dialect로 extends 해주었다.

application.yml 또는 application.properties 설정

application.yml 설정

spring:
  jpa:
    database: mysql
    database-platform: com.xxxxx.xxxxx.xxx.config.CustomMySQLDialect # CustomMySQLDialect 파일 위치 설정해줘야 group_concat() 함수를 사용할 수 있다.

application.properties 설정

spring.jpa.database=mysql
# CustomMySQLDialect 파일 위치 설정해줘야 group_concat() 함수를 사용할 수 있다.
spring.jpa.database-platform=com.xxxxx.xxxxx.xxx.config.CustomMySQLDialect  

group_concat 사용 전 SQL문

  • 위 쿼리문 처럼 group_concat을 하기 전 결과를 보게 되면 출입이력_일련번호유저_일련번호는 같지만 문진표_답변 컬럼의 데이터 값이 달라서 총 3개의 row가 나오게 된다.
  • 내가 원하는 건 출입이력_일련번호유저_일련번호, 문진표_답변한 row 에 나오게 하고 싶다.
  • 해결책은 아래와 같다.

group_concat 사용 후 SQL문

  • 같은 컬럼이지만 데이터가 다른 필드에 group_concat() 을 사용하여 쿼리문을 작성하는 것이다.
  • 위 같이 group_concat() 을 적용하게 되면 같은 컬럼이지만 다른 값으로 나왔던 데이터들이 한 row 에 출력되게 된다.
    • group_concat()GROUP BY 컬럼명 을 이용하여 한 row 에 출력할 수 있다.

QueryDSL에 group_concat() 적용

@Repository
@RequiredArgsConstructor
public class QClassAccessCntrlHistRepository implements ExAccessCntrlHistRepository { 
    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<ResAccessCntrlHistVO> getAccessCntrlList(ReqAccessCntrlHistSearchVO searchVO) {
        QAccessCntrlHist qAccessCntrlHist = QAccessCntrlHist.accessCntrlHist;
        QGateInfo qGateInfo = QGateInfo.gateInfo;
        QUsr qUsr = QUsr.usr;
        QQuestnAnsr qQuestnAnsr = QQuestnAnsr.questnAnsr1;
      
        Pageable pageable = PageRequest.of(searchVO.getStart(), searchVO.getLength()); // Pageable로 만들어주기 위함
      
        return jpaQueryFactory
                .select(
                        Projections.fields(
                                ResAccessCntrlHistVO.class,
                                qAccessCntrlHist.accessCntrlHistSn,
                                qAccessCntrlHist.accessCntrlRegDt,
                                qUsr.usrCameraId,
                                qGateInfo.gateNm,
                                qAccessCntrlHist.img,
                                qUsr.nm,
                                qUsr.mbtlno,
                                qUsr.usrTy,
                                qUsr.mngGradeTy,
                                qAccessCntrlHist.tempr,
                                qAccessCntrlHist.temprStts,
                                qAccessCntrlHist.accessCntrlTy,
                                Expressions.stringTemplate("group_concat({0})", qQuestnAnsr.questnAnsr).as("questnAnsr"),
                                qQuestnAnsr.nm.max().as("qNm"),
                                qQuestnAnsr.mbtlno.max().as("qMbtlno")
                        )
                )
                .from(qAccessCntrlHist)
                .leftJoin(qUsr)
                .on(qAccessCntrlHist.usrSn.eq(qUsr.usrSn))
                .leftJoin(qGateInfo)
                .on(qAccessCntrlHist.gateInfoSn.eq(qGateInfo.gateInfoSn))
                .leftJoin(qQuestnAnsr)
                .on(qAccessCntrlHist.questnAnsrId.eq(qQuestnAnsr.questnAnsrId))
                .where(
                        eqGateInfoSn(searchVO.getGateInfoSn()),
                        betWeenStrAccessCntrlRegDt(searchVO.getSchStrDt(), searchVO.getSchEndDt()),
                        eqAccessCntrlTy(searchVO.getAccessCntrlTy()),
                        likeSearchKeyword(searchVO.getSearchKeyword())
                )
                .groupBy(qAccessCntrlHist.accessCntrlHistSn, qUsr.nm, qQuestnAnsr.nm, qUsr.mbtlno, qQuestnAnsr.mbtlno)
                .orderBy(qAccessCntrlHist.accessCntrlRegDt.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
    }
}
  • select문에 Projections.fields()필드 마지막 Expressions.stringTemplate("group_concat({0})", qQuestnAnsr.questnAnsr).as("questnAnsr") 을 보면 group_concat을 통해 QueryDSL 에서도 MySQLgroup_concat() 함수를 사용할 수 있다.

QueryDSL에 group_concat() 적용 쿼리문 확인

  • 위 사진들 처럼 정상적으로 query문이 날라갔고, JPA QueryDSL 에 정상적으로 바인딩 된 것을 확인 할 수 있다.

  • 정상적으로 같은 컬럼의 각기 다른 값들을 한 row에 문자열 방식으로 출력 되었다.

정리

  • Hibernate 는 특정 데이터베이스에 종속되지 않고 객체지향스럽게 사용할 수 있도록 추상화해주어 특정 DB에 종속된 함수(예, MySQLgroup_concat)는 제공하지 않는다.
  • 때문에 JPA QueryDSL에서 특정 DB의 함수를 사용하려면 설정 해주어야 한다.
  • group_concat() 을 사용할 땐 꼭 GROUP BY 컬럼명 을 이용하여 한 줄(row)에 출력할 수 있게 해야한다.

[JUnit5] JUnit5 AssertJ

[JUnit5] JUnit5 AssertJ

#목차

JUnit5 AssertJ

AssertJ VS Assertions

JUnit 팀의 AssertJ 라이브러리를 사용 권장

JUnit Jupiter에서 제공하는 Assertions 기능만으로도 많은 테스트 시나리오에 충분하지만, 더 강력한 성능과 매처와 같은 추가 기능이 필요하거나 필요한 경우가 있습니다. 이러한 경우 JUnit 팀은 AssertJ, Hamcrest, Truth 등과 같은 타사 어설션 라이브러리를 사용할 것을 권장합니다. 따라서 개발자는 원하는 Assertions 라이브러리를 자유롭게 사용할 수 있습니다. - JUnit5 Doc”

AssertJ의 장점

  • 메소드 체이닝을 지원하기 때문에 좀 더 깔끔하고 읽기 쉬운 테스트 코드를 작성할 수 있다.
  • 개발자가 테스트를 하면서 필요하다고 상상할 수 있는 거의 모든 메소드를 제공한다.

라이브러리 의존성 설정

  • SpringBoot를 사용하여 프로젝트를 생성하게되면 spring-boot-starter-test 라이브러리가 자동으로 들어가게 된다.
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    // ...
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Dependencies

AssertJ VS Assertions 차이점

junit.jupiter의 Assertions

import static org.assertj.core.api.Assertions.assertThat;

class AssertionsTest {
    @Test
    void assertions_테스트() {
        String str1 = "문자열";
        String str2 = "문자열";
        System.out.println("str1 = " + str1.hashCode());
        System.out.println("str2 = " + str2.hashCode());

        Assertions.assertEquals(str1, str2);
    }
}
junit.jupiter의 Assertions 결과

JUnit Assertions

assertj.core의 Assertions

import static org.assertj.core.api.Assertions.*;

public class AssertJTest {

    @Test
    void assertj_테스트() {
        String str1 = "문자열";
        String str2 = "문자열";
        System.out.println("str1 = " + str1.hashCode());
        System.out.println("str2 = " + str2.hashCode());

        assertThat(str1).isEqualTo(str2);
    }
}
assertj.core의 Assertions 결과

AssertJ Assertions

  • actual(실제 값)expexted(기대 값)이 동일한지(isEqualTo()) 확인하는 메서드이다.
  • 아직 까지는 크게 차이나보이진 않는다.
  • 하지만 Collection Test를 보면 크게 다르다는걸 느끼게 될 것이다.

Collection Test

class CollectionTest {

    @Test
    public void junit_assertions_테스트() {
        ArrayList<Student> characters = new ArrayList<>();
        Student spring = new Student("Spring");
        Student jpa = new Student("Jpa");
        Student kubernetes = new Student("Kubernetes");
        characters.add(spring);
        characters.add(jpa);
        characters.add(kubernetes);

        List<Student> expected = new ArrayList<>();
        expected.add(spring);
        expected.add(jpa);
        List<Student> filteredList = characters.stream()
                .filter((character) -> character.name.contains("p"))
                .collect(Collectors.toList());

        Assertions.assertEquals(expected,filteredList);
    }

    static class Student {
        private String name;
        public Student(String name) { this.name = name; }
    }
}
class CollectionTest {

    @Test
    public void assertj_테스트() {
        ArrayList<Student> characters = new ArrayList<>();
        Student spring = new Student("Spring");
        Student jpa = new Student("Jpa");
        Student kubernetes = new Student("Kubernetes");

        characters.add(spring);
        characters.add(jpa);
        characters.add(kubernetes);

        assertThat(characters)
                .filteredOn(character -> character.name.contains("p"))
                .containsOnly(spring, jpa);
    }

    static class Student {
        private String name;
        public Student(String name) { this.name = name; }
    }
}
  • 위 두 테스트 코드를 보게 되면 차이가 많이난다.
  • AssertJ를 통해 filteredOn() 메서드의 람다식을 이용하여 배열 character.name.contains("p")를 이용하여 p가 들어간 것들을 찾는다
  • containsOnly(spring, jpa)를 이용하여 spring변수와 jpa변수에 p가 포함되어있으면 테스트 성공하게 된다.
  • 이러한 기능들 때문에 AssertJ 라이브러리를 많이 사용하게 된다.
Collection Test 결과

Collection Test

AssertJ 기본 검증 메서드

메서드설명
isEqualTo(값)검증대상과 동일한 값인지 비교한다.
isSameAs(값)검증대상과 값을 == 비교한다.
isNotNull()검증대상이 Not Null 인지 확인한다.
isNull()검증대상이 Null 인지 확인한다.
isNotEmpty()검증대상이 Not Empty 인지 확인한다.
isEmpty()검증대상이 Empty 인지 확인한다.
isIn()검증대상이 값 목록에 포함되어 있는지 검증한다.
isNotIn()검증대상이 값 목록에 포함되어 있지 않는지 검증한다.
  • isIn()isNotIn()의 값 목록은 가변 인자(변하는 인자의 개수) 로 주거나 List와 타입을 이용해서 전달한다.
import static org.assertj.core.api.Assertions.assertThat;

class JUnitTest {
    @Test
    void 기본_테스트_메서드() {
        // isEqualTo()
        String str1 = new String("hello");
        String str2 = new String("hello");
        assertThat(str1).isEqualTo(str2); // 비교 대상의 내용이 같은지 확인
        // isSameAs()
        String str3 = str1;
        assertThat(str1).isSameAs(str3); // 같은 객체를 참조하고 있는지 확인

        // isNotNull()
        assertThat(str1).isNotNull(); // 객체가 null이 아닌지 확인

        // isNull()
        String str4 = null;
        assertThat(str4).isNull(); // 객체가 null인지 확인

        // isNotEmpty() and isEmpty()
        List<String> emptyList = new ArrayList<>();
        List<String> nonEmptyList = Arrays.asList("apple", "banana");
        assertThat(emptyList).isEmpty(); // 비어있는지 확인
        assertThat(nonEmptyList).isNotEmpty(); // 비어있지 않은지 확인

        // isIn() and isNotIn()
        String fruit1 = "apple";
        String fruit2 = "orange";
        assertThat(fruit1).isIn(nonEmptyList); // "apple"이 리스트에 포함되어 있는지 확인
        assertThat(fruit2).isNotIn(nonEmptyList); // "orange"가 리스트에 포함되어 있지 않은지 확인
    }
}

AssertJ 기본 검증 메서드 결과

Basic Test

AssertJ isIn(), IsNotIn 메서드 결과

isIn(), isNotIn Test

AssertJ 문자열 검증 메서드

메서드설명
contains(값)검증대상에 (값)이 포함되어있는지 확인한다.
containsOnlyOnce(값)검증대상에 (값)이 딱 한 번만 포함되어있는지 확인한다.
containsOnlyDigits()숫자만 포함하는지 검증한다.
containsWhitespaces()공백 문자를 포함하고 있는지 검증한다.
containsOnlyWhitespaces()공백 문자만 포함하는지 검증한다.
doesNotContain(값)검증대상의 공백 문자만 포함하는지 검증한다.
doesNotContainAnyWhitespaces()검증대상의 공백 문자를 포함하고 있지 않은지를 검증한다.
doesNotContainOnlyWhitespaces()검증대상의 공백 문자만 포함하고 있지 않은지를 검증한다.
doesNotContainPattern(패턴)검증대상의 정규 표현식에 일치하는 문자를 포함하고 있지 않은지를 검증한다.
startsWith(값)검증대상의 시작 값이 (값)과 동일한지 비교한다.
endsWith(값)검증대상의 마지막 값이 (값)과 동일한지 비교한다.
doesNotStartWith(값)검증대상의 (값)이 지정한 문자열로 시작하지 않는지를 검증한다.
doesNotEndWith(값)검증대상의 (값)이 지정한 문자열로 끝나지 않는지를 검증한다.
import static org.assertj.core.api.Assertions.*;

public class AssertJTest {

    @Test
    void 문자열_테스트() {
        assertThat("Hello, world! Nice to meet you.") // 주어진 "Hello, world! Nice to meet you."라는 문자열은
                .isNotEmpty() // 비어있지 않고
                .contains("Nice") // "Nice"를 포함하고
                .contains("world") // "world"도 포함하고
                .doesNotContain("ZZZ") // "ZZZ"는 포함하지 않으며
                .startsWith("Hell") // "Hell"로 시작하고
                .endsWith("u.") // "u."로 끝나며
                .isEqualTo("Hello, world! Nice to meet you."); // "Hello, world! Nice to meet you."과 일치
    }
}

AssertJ 문자열 검증 메서드 결과

String Test

AssertJ 숫자 검증 메서드

메서드설명
isPositive()검증대상이 양수인지 확인한다.
isNotPositive()검증대상이 양수가 아닌지 확인한다.
isNegative()검증대상이 음수인지 확인한다.
isNotNegative()검증대상이 음수가 아닌지 확인한다.
isZero()검증대상이 0 인지 확인한다.
isNotZero()검증대상이 0 이 아닌지 확인한다.
isOne()검증대상이 1 인지 확인한다.
isGraterThan(값)검증대상이 값을 초과한지 확인한다.
isLessThan(값)검증대상이 값보다 미만인지 확인한다.
isGraterThanOrEqualTo(값)검증대상이 값 이상인지 확인한다.
isLessThanOrEqualTo(값)검증대상이 값 이하인지 확인한다.
isBetween(값1, 값2)값1값2 사이에 포함되는지 검증한다.
import static org.assertj.core.api.Assertions.*;

public class AssertJTest {

    @Test
    void 숫자_테스트() {
        assertThat(3.14d) // 주어진 3.14라는 숫자는
                .isPositive() // 양수이고
                .isGreaterThan(3) // 3보다 크며
                .isLessThan(4) // 4보다 작습니다
                .isEqualTo(3, offset(1d)) // 오프셋 1 기준으로 3과 같고
                .isEqualTo(3.1, offset(0.1d)) // 오프셋 0.1 기준으로 3.1과 같으며
                .isEqualTo(3.14); // 오프셋 없이는 3.14와 같습니다
    }
}

AssertJ 숫자 검증 메서드 결과

Number Test

AssertJ 날짜 검증 메서드

메서드설명
isBefore(비교 값)비교 값 보다 이전인지 검증한다. 실제 값 < 비교 값
isBeforeOrEqualTo(비교 값)비교 값 보다 이전이거나 같은지 검증한다. 실제 값 <= 비교 값
isAfter(비교 값)비교 값 보다 이후인지 검증한다. 실제 값 > 비교 값
isAfterOrEqualTo(비교 값)비교 값 보다 이후이거나 같은지 검증한다. 실제 값 >= 비교 값

AssertJ 날짜 검증 메서드 결과

Compare Test

as - Fail Message

  • as(String description, Object... args)를 사용하여 테스트 코드의 실패 메시지를 설정할 수 있다.
  • as는 검증 문보다 앞에 작성해야 하며, 그렇지 않을 경우 검증 문 이후 호출이 중단됨으로 무시된다.
import static org.assertj.core.api.Assertions.*;

class FailMessageTest {
    
    @Test
    void fail_message_테스트() {
        String str = "JUnit";
        assertThat(str)
                // as는 검증 문보다 앞에 작성해야 하며, 그렇지 않을 경우 검증 문 이후 호출이 중단됨으로 무시된다.
                .as("기대값(Expected) AssertJ와 실제값(Actual) %s이 일치하지 않습니다.", str)
                .isEqualTo("AssertJ");
    }
}

as - Fail Message 결과

Fail Message Test

filteredOn 메서드

import static org.assertj.core.api.Assertions.*;

class FilteredOnTest {
    public Member member1, member2, member3;
    public List<Member> members;

    static class Member {
        private String name;
        private int age;
        private MemberRole role;

        public Member(String name, int age, MemberRole role) {
            this.name = name;
            this.age = age;
            this.role = role;
        }

        public String getName() { return name; }
        public int getAge() {return age; }
        public MemberRole getRole() { return role; }
    }

    enum MemberRole { ADMIN, BASIC, VIP }

    @BeforeEach
    public void createMember() {
        member1 = new Member("Kim", 20, MemberRole.ADMIN);
        member2 = new Member("Ahn", 20, MemberRole.BASIC);
        member3 = new Member("Park", 21, MemberRole.VIP);
        members = Lists.list(member1, member2, member3);
    }

    @Test
    public void 체이닝_람다를_사용한_필터_테스트() {
        assertThat(members) // members 컬렉션에서
                .filteredOn(member -> member.getAge() >= 20) // age >= 20 인 객체만 필터링
                .filteredOn(member -> !member.getName().equals("Kim")) // name이 "Kim"과 같지 않은 객체만 필터링
                .filteredOn("role", in(MemberRole.VIP)) // "role"이 MemberRole.VIP인 객체를 필터링
                .containsOnly(member3) // 그 객체가 오직 member3 이며,
                .isNotEmpty(); // 비어있지 않다.
    }
}
  • 위 테스트 처럼 체이닝과 람다를 활용하여 더욱 편리하게 filteredOn() 메서드를 사용할 수 있다.

AssertJ filteredOn 메서드 결과

Fail Message Test

Extracting 메서드

  • extracting() 메서드의 파라미터로 필드명 을 입력하거나, 메서드 레퍼런스 도 표현이 가능하다.
import static org.assertj.core.api.Assertions.*;

class FilteredOnTest {
    public Member member1, member2, member3;
    public List<Member> members;

    class Member {
        private String name;
        private int age;
        private MemberRole role;

        public Member(String name, int age, MemberRole role) {
            this.name = name;
            this.age = age;
            this.role = role;
        }

        public String getName() { return name; }
        public int getAge() {return age; }
        public MemberRole getRole() { return role; }
    }

    enum MemberRole { ADMIN, BASIC, VIP }

    @BeforeEach
    public void createMember() {
        member1 = new Member("Kim", 20, MemberRole.ADMIN);
        member2 = new Member("Ahn", 20, MemberRole.BASIC);
        member3 = new Member("Park", 21, MemberRole.VIP);
        members = Lists.list(member1, member2, member3);
    }

    @Test
    public void 메서드_레퍼런스를_이용한_필터_테스트() {
        assertThat(members) // members 컬렉션에서
                .filteredOn(
                        member -> member.getAge() >= 20 // member 객체의 age가 >= 20 인 객체만 필터링
                                && !member.getName().equals("Kim") // member 객체의 name이 "Kim"과 같지 않은 객체만 필터링
                                && member.getRole().equals(MemberRole.VIP) // member 객체의 role이 MemberRole.VIP인 객체를 필터링
                )
                .extracting(
                        Member::getName, // member -> member.getName() => Member 클래스 객체의 getName() 메서드를 참조
                        Member::getAge, // member -> member.getAge()
                        Member::getRole // member -> member.getRole()
                )
                .containsExactly(tuple("Park", 21, MemberRole.VIP)); // containsExactly(): 순서 원소의 개수와 값이 일치해야 한다.
    }
}
  • @BeforeEach를 통해 @Test 어노테이션이 붙은 테스트 메서드를 실행하기 전 createMember() 메서드를 실행하여 Member 객체들을 생성한다.
  • 이후 extracting() 메서드에서 람다 표현식더 간단하게 표현하는 방법인 메서드 레퍼런스를 이용하여 Member 객체의 메서드를 참조(Method Reference)를 이용하여 .filteredOn()을 구현한다.
  • 마지막 .containsExactly() 메서드에 인자로 tuple()을 사용하여 각기 다른 인자값들을 받아 테스트 하게 되는데, 이때 .containsExactly()는 순서 원소의 개수와 값이 모두 일치해야 한다.

Extracting 메서드 결과

Fail Message Test

assertThatThrownBy() : 예외처리

assertThatThrownBy() : 예외처리 에러1

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class AssertThrowsTest {

    @Test
    void assertThrows_테스트() {
        // given
        String input = "abc";

        // when, then : 입력값 범위 밖일 경우 StringIndexOutOfBoundsException 발생
        assertThatThrownBy(() -> input.charAt(input.length()))
                .isInstanceOf(StringIndexOutOfBoundsException.class)
                .hasMessageContaining("String index out of length");
    }
}
  • 에러 메세지의 값이 "String index out of range" 가 포함되어 있어야 하는 테스트이지만 "String index out of length" 여서 테스트가 실패하게 된다.

assertThatThrownBy() - 예외처리 에러1 결과

AssertThatThrownBy Fail Test1

assertThatThrownBy() - 예외처리 에러2

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class AssertThrowsTest {

    @Test
    void assertThrows_테스트() {
        // given
        String input = "abc";

        // when, then : 입력값 범위 밖일 경우 StringIndexOutOfBoundsException 발생
        assertThatThrownBy(() -> input.charAt(input.length()))
                .isInstanceOf(StringIndexOutOfBoundsException.class)
                .hasMessageContaining(String.valueOf(4));
    }
}
  • 에러 메세지의 값이 "String index out of range: 3" 가 포함되어 있어야 하는 테스트이지만 "4" 여서 테스트가 실패하게 된다.

assertThatThrownBy() - 예외처리 에러2 결과

AssertThatThrownBy Fail Test2

assertThatThrownBy() - 예외처리 성공

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class AssertThrowsTest {

    @Test
    void assertThrows_테스트() {
        // given
        String input = "abc";

        // when, then : 입력값 범위 밖일 경우 StringIndexOutOfBoundsException 발생
        assertThatThrownBy(() -> input.charAt(input.length()))
                .isInstanceOf(StringIndexOutOfBoundsException.class)
                .hasMessageContaining("String index out of range")
                .hasMessageContaining(String.valueOf(input.length()));
    }
}
  • 에러 메세지의 값이 "String index out of range: 3" 가 포함되어야하는 값에 각각 "String index out of range""3"이 포함되어 테스트가 성공하게 된다.

assertThatThrownBy() - 예외처리 결과

AssertThatThrownBy Test

Back to [JUnit5] JUnit5

Reference

[JUnit5] JUnit5 어노테이션2

[JUnit5] JUnit5 어노테이션2

#목차

JUnit5 Annotations ver2

@Disabled

  • 테스트를 하고 싶지 않은 클래스나 메서드에 붙이는 어노테이션
  • JUnit4@Ignore과 유사
class DisabledExampleTest {
    
    @Test
    @Disabled("문제가 해결될 때까지 테스트 중단")
    void 테스트1() {
        System.out.println("테스트1");
    }
    
    @Test
    void 테스트2() {
        System.out.println("테스트2");
    }
}

@Disabled 결과

@Disabled Annotation

@Nested

  • 정적(static)이 아닌 중첩 클래스를 나타내는 어노테이션
  • Java8 부터 Java15 까지는 테스트 인스턴스 라이프사이클을 사용하지 않는 한 @BeforeAll@AfterAll의 메서드를 @Nested 테스트 클래스안에서 직접 사용할 수 없다.
  • 단, Java16 부터는 테스트 인스턴스 라이프사이클 중에 하나를 사용하여 @Nested 테스트 클래스 에서 @BeforeAll@AfterAll의 메서드를 정적(static) 으로 선언 할 수 있다.
public class NestedExampleTest {

    @Nested
    class 중첩_클래스_1 {
        @Test
        public void 성공() { }

        @Test
        public void 실패() { }
    }

    @Nested
    class 중첩_클래스_2 {

        @Nested
        class 테스트1 {
            @Test
            public void 성공() { }

            @Test
            public void 실패() { }
        }

        @Nested
        class 테스트2 {
            @Test
            public void 성공() { }

            @Test
            public void 실패() { }
        }
    }
}

@Nested 결과

@Nested Annotation

@Tag

  • 테스트 그룹을 만들고 원하는 테스트 그룹만 테스트를 실행할 수 있는 어노테이션.
  • 클래스 또는 메서드 레벨에 사용되며, 해당 어노테이션은 클래스 수준에서는 상속 되지만, 메서드 수준에서는 상속되지 않는다.
class TagExampleTest {

    @Test
    @Tag("top")
    void top_line() { }

    @Test
    @Tag("jungle")
    void jungle_line() { }

    @Test
    @Tag("mid")
    void mid_line() { }

    @Test
    @Tag("ad")
    void ad_line() { }

    @Test
    @Tag("support")
    void support_line() { }
}

@Tag 결과

  • 테스트 하기 전에 intelliJ IDE에서는 내가 원하는 @Tag("xxx"")에 지정해준 테스트만을 하기 위해선 설정을 해주어야 한다.

    intelliJ IDE 우측 상단 Edit Configurations... 클릭

    Build and Run 에서 Method 선택되어 있는 부분을 Tags 선택

    Tags 선택 후 내가 테스트할 메서드 (@Tag("키워드"), 나의 예제에선 @Tag("top"))의 태그 이름을 넣어준다

    @Tag Annotation

Custom @Tag

  • 커스텀 태그는 직접 특정한 태그를 만들어 사용하는 방식. 이렇게 미리 커스텀 태그를 만들고, 여러번 사용하면 코드의 중복과 태그의 오타가 발생할 확률을 줄일 수 있다.

1. Custom @Tag 만들기

@Retention(RetentionPolicy.RUNTIME) // 해당 어노테이션이 붙은 메서드가 런타임 될 동안까지 실행
@Target(value = {ElementType.TYPE, ElementType.METHOD}) // 메서드에서 사용할 것이이라고 지정
@Tag("TopLine")
public @interface TopLine {
}

Custom @Tag TopLine 세팅

Custom @Tag Annotation Settings

Custom @Tag 결과 1

Custom @Tag Annotation Result

나머지

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Tag("JungleLine")
public @interface JungleLine { }

@Retention(RetentionPolicy.RUNTIME) 
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Tag("MidLine")
public @interface MidLine { }

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Tag("AdLine")
public @interface AdLine { }

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Tag("SupportLine")
public @interface SupportLine { }

나머지 결과

Custom @Tag Annotation Settings

Custom @Tag Annotation Result

Custom @Tag Annotation Settings

Custom @Tag Annotation Result

Custom @Tag Annotation Settings

Custom @Tag Annotation Result

Custom @Tag Annotation Settings

Custom @Tag Annotation Result

class TagExampleTest {

    @Test
    @TopLine
    void top_line() { }

    @Test
    @JungleLine
    void jungle_line() { }

    @Test
    @MidLine
    void mid_line() { }

    @Test
    @AdLine
    void ad_line() { }

    @Test
    @SupportLine
    void support_line() { }
}

@Timeout

  • @Timeout 어노테이션을 사용하면 @Test, @TestFactory, @TestTemplate 또는 라이프사이클 메서드의 실행 시간이 지정된 기간을 초과하는 경우 실패하도록 선언할 수 있다.
  • 기간의 시간 단위는 기본값이 second(초)이지만 사용자가 따로 시간을 지정할 수 있다.
class TimeoutExampleTest {

    @BeforeEach
    @Timeout(5)
    void setUp() {
        // 실행 시간이 5초를 초과하면 실패
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds500Milliseconds() {
        // 실행 시간이 500밀리초(0.5초)를 초과하면 실패
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
    void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
        // 실행 시간이 500밀리초(0.5초)를 초과하면 실패하고 테스트 코드가 별도의 스레드에서 실행된다.
    }
}
  • 테스트 클래스 내의 모든 테스트 메서드와 모든 @Nested 클래스에 동일한 시간 제한을 적용하려면 클래스 수준에서 @Timeout 어노테이션을 선언하면 된다. 그러면 특정 메서드나 @Nested 클래스에 대한 @Timeout 어노테이션으로 재정의하지 않는 한 해당 클래스 및 해당 @Nested 클래스 내의 모든 테스트, 테스트 팩토리 및 테스트 템플릿 메서드에 적용된다. 클래스 수준에서 선언된 @Timeout 어노테이션은 라이프사이클 메서드에는 적용되지 않는다는 점에 유의.
  • TestFactory 메서드에 @Timeout을 선언하면 팩토리 메서드가 지정된 기간 내에 반환되는지 확인하지만 팩토리에서 생성된 각 개별 DynamicTest의 실행 시간은 확인하지 않는다. 이를 위해 assertTimeout() 또는 assertTimeoutPreemptively()를 사용하면 된다.
  • Timeout@TestTemplate 메서드에 있는 경우(예: @RepeatedTest 또는 @ParameterizedTest), 각 호출에 지정된 시간 제한이 적용된다.

@Timeout 예제 코드

class TimeoutExampleTest {

    @BeforeAll
    static void testBeforeAll() {
        System.out.println("BeforeAll");
    }
    
    @AfterAll
    static void testAfterAll() {
        System.out.println("AfterAll");
    }
    
    @Timeout(1) // 1초 지나면 테스트 실패
    @Test
    void timeoutException() throws InterruptedException {
        System.out.println("timeoutException");
        assertEquals("timeout", "timeout");
        Thread.sleep(2000); // 2초동안 스레드 sleep
    }
}

@Timeout 예제 코드 결과

@Timeout Annotation Result

@Timeout 구성 파라미터 값 예시

매개변수 값@어노테이션 설정
42@Timeout(42)
42 ns@Timeout(value = 42, unit = NANOSECONDS)
42 μs@Timeout(value = 42, unit = MICROSECONDS)
42 ms@Timeout(value = 42, unit = MILLISECONDS)
42 s@Timeout(value = 42, unit = SECONDS)
42 m@Timeout(value = 42, unit = MINUTES)
42 h@Timeout(value = 42, unit = HOURS)
42 d@Timeout(value = 42, unit = DAYS)

@ExtendWith

  • 단위 테스트에 공통적으로 사용할 확장 기능을 선언해주는 역할을 한다.
  • 인자로 확장할 Extension을 명시하면 된다.
  • SpringExtension.class 또는 MockitoExtension.class를 많이 사용한다.
  • Spring Test Context 프레임워크와 Junit5와 통합해 사용할 때는 SpringExtension.class를 사용한다.
  • JUniit5Mockito를 연동해 테스트를 진행할 경우에는 MockitoExtension.class를 사용한다.
@ExtendWith(SpringExtension.class)
class ExtendWithExampleTest { }

@ExtendWith(MockitoExtension.class)
class ExtendWithExampleTest { }

@ExtendWith - 기본 확장 모델 1

public class ExtensionExample implements BeforeEachCallback, AfterEachCallback {
    @Override
    public void beforeEach(final ExtensionContext context) throws Exception {
        System.out.println("ExtensionExample.beforeEach");
    }

    @Override
    public void afterEach(final ExtensionContext context) throws Exception {
        System.out.println("ExtensionExample.afterEach");
    }
}
  • 여기서 Extension 자체는 마커 인터페이스이어서 메서드가 정의되어 있지 않다.

마커 인터페이스(marker interface)란, 일반적인 인터페이스와 동일하지만, 아무 메서드도 선언하지 않은 인터페이스이다. 자바의 대표적인 마커 인터페이스로는 Serializable, Cloneable, EventListener가 있다. 대부분의 경우 마커 인터페이스를 단순한 타입 체크를 하기 위해 사용한다.

  • Extension 상속한 BeforeEachCallback 이나 AfterEachCallback와 같은 확장 포인트 마다 정의된 서브 인터페이스를 구현한다.
    • BeforeEachCallback는 각 테스트 전에 콜백되는 `beforeEach() 메소드가 정의된다.
    • AfterEachCallback는 각 테스트 후에 콜백되는 afterEach() 메소드가 정의된다.
  • 확장 기능을 구현한 클래스를 실제로 테스트에서 사용하는 방법 중 하나로 @ExtendWith 어노테이션을 사용하는 방법이 있다.
  • 확장 기능을 적용할 위치에 @ExtendWith 어노테이션을 설정하고, value에 적용할 확장 프로그램의 Class 객체를 지정한다.
@ExtendWith(ExtensionExample.class)
class ExtensionExampleTest {
    @Test
    void test1() {
        System.out.println("  test1()");
    }

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

@ExtendWith - 기본 확장 모델 1 결과

@ExtendWith Annotation Result

@ExtendWith - 기본 확장 모델 2

// @ExtendWith(ExtensionExample.class)
class ExtensionExampleTest {
    
    @Test
    @ExtendWith(ExtensionExample.class)
    void test1() {
        System.out.println("  test1()");
    }

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

@ExtendWith - 기본 확장 모델 2 결과

@ExtendWith Annotation Result

@RegisterExtension

확장 기능을 절차적으로 등록

  • @ExtendWith을 사용하는 방법의 경우 확장 기능 클래스의 조정은 기본적으로 정적이다.
    • 확장 기능을 구현한 클래스의 인스턴스는 Jupiter 에 의해 안에서 생성된다.
    • 그러기 때문에, 확장 기능 클래스의 인스턴스에 대해서 세세한 조정은 기본적으로 할 수 없다.
  • 반면에 @RegisterExtension을 사용하면 확장 클래스의 조정을 동적으로 지정할 수 있다.
public class RegisterExtensionExample implements BeforeEachCallback, BeforeAllCallback {
    private final String name;

    public RegisterExtensionExample(final String name) { this.name = name; }

    @Override
    public void beforeAll(final ExtensionContext context) throws Exception {
        System.out.println("[" + this.name + "] beforeAll()");
    }

    @Override
    public void beforeEach(final ExtensionContext context) throws Exception {
        System.out.println("[" + this.name + "] beforeEach()");
    }
}
public class RegisterExtensionExampleTest {

    @RegisterExtension
    static RegisterExtensionExample classField = new RegisterExtensionExample("classField");

    @RegisterExtension
    RegisterExtensionExample instanceField = new RegisterExtensionExample("instanceField");

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

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

    @Test
    void test1() { System.out.println("test1()"); }
}
  • 사용할 확장 클래스의 인스턴스를 확장을 사용하려는 테스트 클래스의 필드(static or 인스턴스)로 선언한다.
    • 이 필드를 @RegisterExtension으로 어노테이션을 지정하면 해당 필드에 설정된 인스턴스를 확장으로 등록할 수 있다.
    • 필드에 대한 인스턴스 설정은 모든 프로그램에서 작성할 수 있으므로 자유롭게 조정된 인스턴스를 사용할 수 있다.
  • static으로 선언한 필드를 사용한 경우는 모든 확장 프로그램을 사용할 수 있다.
  • 인스턴스 필드를 사용하는 경우 BeforeAllCallback와 같은 클래스 수준 확장 기능과 TestInstancePostProcessor와 같은 인스턴스 수준 확장 기능을 사용할 수 없다.
    • 구현하더라도 무시된다.
    • BeforeEachCallback와 같은 메소드 레벨 확장 기능을 사용할 수 있다.

@RegisterExtension 결과

@ExtendWith Annotation Result

@TempDir

  • TempDirectory extension은 테스트클래스 안에 있는 독립적인 테스트 또는 모든 테스트에 대해 임시 디렉토리를 생성하고 정리를 할 때 사용한다.
  • 이 기능을 사용하려면 접근 제어자가 private이 아닌 java.nio.file.Pathjava.io.File 필드에 @TempDir 어노테이션을 붙이거나, 파라미터에 붙여준다.
class TempDirExampleTest {
    @TempDir
    File tempFolderFile;

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

    @Test
    void test2(@TempDir Path tempFolderPath) {
        System.out.println(tempFolderFile.getAbsolutePath());
    }
}

@TempDir 결과

@ExtendWith Annotation Result

Dynamic Test

  • 일반적으로 사용되는 @Test를 이용한 테스트는 컴파일 시점에 완전히 지정되는 정적 테스트. 이는 동적인 기능에 대한 기본 테스트 형태를 제공하지만, 그 표현이 컴파일 시점에 제한된다는 한계를 가지고 있다.
  • 이에 비해 Dynamic TestRuntime 동안에 테스트가 생성되고 수행되기 때문에, 프로그램이 수행되는 도중에도 동작을 변경할 수 있는 특징이 있다.

@TestFactory

Continue with [JUnit5] Dynamic Test @TestFactory

@TestTemplate

  • @Test 메서드와 달리 @TestTemplate은 그 자체가 테스트 케이스가 아니라 테스트 케이스에 대한 템플릿이다. 따라서 등록된 프로바이더가 반환하는 호출 컨텍스트의 수에 따라 여러 번 호출되도록 설계되었다.
  • 하나 이상의 프로바이더와 함께 사용해야하며, 그렇지 않으면 실행이 실패한다.

  • @TestTemplate 메서드는 일반 테스트 케이스가 아니라 테스트 케이스를 위한 템플릿이다.
  • 따라서 등록된 프로바이더가 반환하는 호출 컨텍스트의 수에 따라 여러 번 호출되도록 설계되었다.
  • 따라서 등록된 TestTemplateInvocationContextProvider 확장과 함께 사용해야 한다.
  • @TestTemplate 메서드의 각 호출은 동일한 수명 주기 콜백 및 확장을 완벽하게 지원하면서 일반 @Test 메서드의 실행과 같이 동작한다.
public class TestTemplateTest {
    final List<String> fruits = Arrays.asList("apple", "banana", "lemon");

    @TestTemplate
    @ExtendWith(MyTestTemplateInvocationContextProvider.class)
    void testTemplate(String fruit) {
        assertTrue(fruits.contains(fruit));
    }

    public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
        @Override
        public boolean supportsTestTemplate(ExtensionContext context) { return true; }

        @Override
        public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
            return Stream.of(invocationContext("apple"), invocationContext("banana"));
        }

        private TestTemplateInvocationContext invocationContext(String parameter) {
            return new TestTemplateInvocationContext() {
                @Override
                public String getDisplayName(int invocationIndex) { return parameter; }

                @Override
                public List<Extension> getAdditionalExtensions() {
                    return Collections.singletonList(new ParameterResolver() {
                        @Override
                        public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                            return parameterContext.getParameter().getType().equals(String.class);
                        }

                        @Override
                        public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                            return parameter;
                        }
                    });
                }
            };
        }
    }

    @Test
    void testTemplateTest() {
        testTemplate("apple");
    }
}

@TestTemplate 결과

@TestTemplate Annotation Result

@TestClassOrder

  • 테스트 클래스에서 @Nested 테스트 클래스에 대한 테스트 클래스 실행 순서를 구성하는 데 사용된다.
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class TestClassOrderExampleTest {
    @Nested
    @Order(1)
    class PrimaryTests {
        @Test
        void test1() {}
    }

    @Nested
    @Order(2)
    class SecondaryTests {
        @Test
        void test2() {}
    }
}

## @TestClassOrder 결과

@TestClassOrder Annotation Result

@TestClassOrder Annotation Result

@TestMethodOrder

  • 테스트 클래스에 대한 테스트 메서드 실행 순서를 구성하는 데 사용된다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class TestMethodOrderExampleTest {
    @Test
    @Order(1)
    void test1() {}

    @Test
    @Order(2)
    void test2() {}

    @Test
    @Order(3)
    void test3() {}
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class TestMethodOrderExampleTest {
    @Test
    @Order(2)
    void test1() {}

    @Test
    @Order(3)
    void test2() {}

    @Test
    @Order(1)
    void test3() {}
}

## @TestMethodOrder 결과

@TestMethodOrder Annotation Result

@TestMethodOrder Annotation Result

@TestInstance

  • @TestInstance는 어노테이션이 지정된 테스트 클래스 또는 테스트 인터페이스에 대한 테스트 인스턴스의 라이프사이클을 구성하는데 사용된다.
  • 테스트 클래스에 @TestInstance가 명시적으로 지정되지 않은 경우, 기본적으로 PER_METHOD 를 사용한다.
enum설명
Lifecycle.PER_METHOD@Test 메서드, @TestFactory 메서드 또는 @TestTemplate 메서드에 대해 새 테스트 인스턴스가 생성된다.
Lifecycle.PER_CLASS테스트가 아니라 테스트 클래스 별로 테스트 인스턴스가 생성된다.
class TestInstanceExampleTest {
    int number = 0;

    @Test
    void add1() {
        number++;
        System.out.println("add1() number = " + number);
        System.out.println(this);
    }

    @Test
    void add2() {
        number += 2;
        System.out.println("add2() number = " + number);
        System.out.println(this);
    }
}

@TestInstance(Lifecycle.PER_METHOD) 결과

@TestInstance(Lifecycle.PER_METHOD) Annotation Result

  • 위 결과와 같이 @TestInstance를 선언하지 않으면 @TestInstance(Lifecycle.PER_METHOD)로 지정되어 각 메서드가 다른 인스턴스인것을 확인할 수 있다.
  • 또한 각기 다른 인스턴스이기 때문에 number의 값이 1과 2가 나온것을 확인 할 수 있다.
  • 위와 다르게 같은 인스턴스를 사용하게 하려면 @TestInstance(Lifecycle.PER_CLASS) 를 지정해주면 된다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestInstanceExampleTest {
    int number = 0;

    @Test
    void add1() {
        number++;
        System.out.println("add1() number = " + number);
        System.out.println(this);
    }

    @Test
    void add2() {
        number += 2;
        System.out.println("add2() number = " + number);
        System.out.println(this);
    }
}

@TestInstance(Lifecycle.PER_CLASS) 결과

@TestInstance(Lifecycle.PER_CLASS) Annotation Result

Lifecycle.PER_CLASS 는 언제 사용할까?

  • 이 어노테이션은 모든 테스트 전에 클래스를 인스턴스화하는 데 비용이 많이 드는 경우에 유용하다. 예를 들어 데이터베이스 연결을 설정하거나 대용량 파일을 로드할 수 있다.
  • 상태 공유는 일반적으로 단위 테스트에서는 안티 패턴이지만 통합 테스트에서는 유용할 수 있다.
  • 클래스별 수명 주기는 의도적으로 상태를 공유하는 순차 테스트를 지원하며, 이는 특히 테스트 중인 시스템을 올바른 상태로 만드는 속도가 느린 경우, 이후 테스트에서 이전 테스트의 단계를 반복해야 하는 것을 피하기 위해 필요할 수 있다.

Back to [JUnit5] JUnit5 어노테이션 ver1

Back to [JUnit5] JUnit5

Reference

[JUnit5] @TestFactory

[JUnit5] @TestFactory

#목차

JUnit5 Annotations

Dynamic Test

‘Dynamic Test`는 Runtime 시점에 생성’

  • 일반적으로 사용되는 @Test를 이용한 테스트는 컴파일 시점에 완전히 지정되는 정적 테스트. 이는 동적인 기능에 대한 기본 테스트 형태를 제공하지만, 그 표현이 컴파일 시점에 제한된다는 한계를 가지고 있다.
  • 이에 비해 Dynamic TestRuntime 동안에 테스트가 생성되고 수행되기 때문에, 프로그램이 수행되는 도중에도 동작을 변경할 수 있는 특징이 있다.

@TestFactory

  • @TestFactory는 테스트 케이스를 동적으로 생성할 수 있다.
  • @Test 메서드와 달리 @TestFactory는 테스트 케이스 자체가 아니라 테스트 케이스를 위한 Factory이다.
  • @TestFactory 메서드는 private 또는 static이어서는 안도 된다.
  • 또한 Stream, Collection, Iterable, Iterator, 또는 DynamicNode 인스턴스배열반환해야 한다.
  • 컴파일 시점에 유효하지 않은 반환 유형을 감지할 수 없기 때문에, 다른 형태로 반환하면 JUnitException이 발생한다는 특징이 있다.

“규모가 큰 프로젝트에서는 각각의 기능에 따라 담당자가 나누어져 있기 때문에 이러한 시나리오 테스트보다는 단위 테스트가 더 필요하고 적합할 것이다. 또한 시나리오 테스트를 하고싶어도 다른 사람이 짠 코드를 같이 테스트 해야하기 때문에 여러가지 걸림돌이 있을 것이다. 하지만 규모가 작은 프로젝트의 경우 개발 인원이 적고, 한 사람이 여러가지 기능을 맡을 것이기 때문에 각 기능을 쪼개서 테스트하는 단위 테스트보다 시나리오 테스트가 더 적합할 수 있다.”

public class TestFactoryExample {
    private final Calculator calculator = new Calculator();

    // JUnitException 발생
    @TestFactory // @TestFactory 어노테이션을 사용할 때,@TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }
    
    @TestFactory // Collection<DynamicTest>를 반환하는 @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
                dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }

    @TestFactory // Iterable<DynamicTest>를 반환하고, Arrays.asList()를 사용하여 테스트 케이스를 생성
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
                dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }

    @TestFactory // Iterator<DynamicTest>를 반환하며, Arrays.asList().iterator()를 사용하여 테스트 케이스를 생성
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
                dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        ).iterator();
    }

    @TestFactory // DynamicTest[]를 반환하고, 테스트 케이스를 배열로 생성
    DynamicTest[] dynamicTestsFromArray() {
        return new DynamicTest[] {
                dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        };
    }

    @TestFactory // 특정 문자열에 대해 isPalindrome 검사를 수행하는 동적 테스트를 반환하는 Stream<DynamicTest>를 생성
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("racecar", "radar", "mom", "dad")
                .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
    }

    @TestFactory // 처음 10개의 짝수 정수에 대한 동적 테스트를 생성하는 Stream<DynamicTest>를 반환
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // 처음 10개의 짝수 정수에 대한 테스트를 생성
        return IntStream.iterate(0, n -> n + 2)
                .limit(10)
                .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory // 무작위로 생성된 정수들에 대해 동적 테스트를 생성하는 Stream<DynamicTest>를 반환
    Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
        // 0에서 100 사이의 임의의 양의 정수를 생성
        // 7로 균등하게 나누어지는 숫자를 만듦
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {
            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };

        // 입력:5, 입력:37, 입력:85 등과 같은 표시 이름을 생성
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;

        // 현재 입력 값을 기준으로 테스트를 실행
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);

        // 동적 테스트 스트림을 반환
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory // 주어진 문자열이 회문인지 확인하는 동적 테스트를 반환하는 Stream<DynamicTest>를 생성
    Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
        // 확인 할 회문(e.g) 기러기, 토마토, 등등)을 Stream으로 저장 
        Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");

        // 다음과 같은 표시 이름 생성 : racecar는 회문(e.g) 기러기, 토마토, 등등)이다
        Function<String, String> displayNameGenerator = text -> text + " is a palindrome";

        // 현재 입력 값을 기준으로 테스트를 실행
        ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));

        // 동적 테스트 스트림을 반환
        return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
    }

    @TestFactory // 이름이 지정된 동적 테스트를 반환하는 Stream<DynamicTest>를 생성
    Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
        // 확인 할 회문(e.g) 기러기, 토마토, 등등)을 Stream으로 저장
        Stream<Named<String>> inputStream = Stream.of(
                named("racecar is a palindrome", "racecar"),
                named("radar is also a palindrome", "radar"),
                named("mom also seems to be a palindrome", "mom"),
                named("dad is yet another palindrome", "dad")
        );

        // 동적 테스트 스트림을 반환
        return DynamicTest.stream(inputStream, text -> assertTrue(isPalindrome(text)));
    }

    @TestFactory // DynamicNode를 반환하는 동적 테스트를 생성
    Stream<DynamicNode> dynamicTestsWithContainers() {
        // 여기서는 "A", "B", "C" 에 대한 동적 컨테이너를 생성하며, 이 컨테이너 안에는 문자열 관련 성질을 검사하는 다른 동적 테스트들이 포함
        return Stream.of("A", "B", "C")
                .map(input -> dynamicContainer("Container " + input, Stream.of(
                        dynamicTest("not null", () -> assertNotNull(input)),
                        dynamicContainer("properties", Stream.of(
                                dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                                dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                        ))
                )));
    }

    @TestFactory // 단일 동적 테스트 DynamicNode를 반환하며, "pop" 문자열이 회문(e.g) 기러기, 토마토, 등등)인지 확인
    DynamicNode dynamicNodeSingleTest() {
        return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
    }

    @TestFactory // 하나의 동적 컨테이너 DynamicNode를 반환하며, 다양한 문자열에 대해 회문 검사하는 동적 테스트들이 포함
    DynamicNode dynamicNodeSingleContainer() {
        return dynamicContainer("palindromes",
                Stream.of("racecar", "radar", "mom", "dad")
                        .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))));
    }

    // 주어진 문자열이 회문인지 확인하는 로직을 구현한 것입니다. 단어가 거꾸로 읽어도 동일한 문자열인 경우, 회문으로 판단
    private static boolean isPalindrome(String str) {
        // 원래 str의 역순을 저장하기 위해 빈 문자열 초기화
        String rev = "";

        // 답변에 대한 새 boolean 변수 초기화
        boolean ans = false;
        // 역순으로 문자열 저장
        for (int i = str.length() - 1; i >= 0; i--) {
            rev = rev + str.charAt(i);
        }
        // 두 문자열이 같은지 확인
        if (str.equals(rev)) {
            ans = true;
        }
        return ans;
    } 
    
}

@TestFactory 테스트 결과

@TestFactory 테스트결과

dynamicTestsWithInvalidReturnType() Error 로그

org.junit.platform.commons.JUnitException: @TestFactory method [java.util.List<java.lang.String> com.example.junit5example.junit5test.annotations.dynamicTest.TestFactoryTest.dynamicTestsWithInvalidReturnType()] must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.

Error Log 해설

  • 위 코드에서 발생하는 JUnitException@TestFactory 어노테이션을 사용하여 작성된 메소드(dynamicTestsWithInvalidReturnType())의 리턴 타입이 유효하지 않기 때문이다.
  • @TestFactory 어노테이션을 사용할 때, 메소드의 리턴 타입은 반드시 Collection, Iterable, Iterator, Stream 중 하나의 인터페이스를 구현해야 하며, 원소는 DynamicNode 타입이어야 한다.
  • 즉, 리턴 타입이 Collection, Iterable, Iterator, Stream 중 하나가 아니라 List<String> 타입으로 리턴되어 JUnitException이 발생한 것이다.

Back to [JUnit5] JUnit5 어노테이션

Reference

Dynamic Test

[Routine] 26 주차 시작!

[Routine] 26 주차 시작!

2023년 07월 10일 부터 07월 14 까지의 나의 루틴.

#목차

26주차

2023-07-10

  • 오늘도 어김없이 영한 님의 인강과 함께 출근하였다.

2023-07-11

  • 오늘도 역시나 영한 님의 인강과 함께!

2023-07-12

  • 앞으로 루틴에 대해 더 이상 포스팅 하지 않고 깃 커밋으로 대체하려 한다.
  • 또한 루틴 포스트에서 작성한 내용들을 깃으로 다 넘기려 한다.
  • 블로그 루틴 포스팅을 지우고 한달에 한 번 정도의 자료 조사를 통해 블로그 포스팅을 하려한다.

Back to [Routine] 25 주차 시작!

[Routine] 25 주차 시작!

[Routine] 25 주차 시작!

2023년 07월 03일 부터 07월 09 까지의 나의 루틴.

#목차

25주차

2023-07-03

  • 오늘은 개발바닥의 이력서 주제의 유튜브를 보며 출근하였다.
  • 느낀 점은 나 또한 너무 넓이만 신경 쓰고 깊이에 신경을 안 쓰는 거 같다는 생각을 하였다.
  • 다시 한번 깊이에 신경 쓰는 공부를 해야겠다고 생각하였다.

2023-07-04

  • 오늘도 개발바닥의 이력서 영상을 보며 출근하였다.
  • 신입, 경력 분 들의 실력들이 정말 대단하다고 다시 한번 느꼈다.
  • 역시 세상에는 열심히 꾸준히 하는 사람들이 정말 많구나 느꼈다.

2023-07-05

  • 오늘도 어김없이 개발바닥의 이력서 영상을 보면서 출근하였는데, 그 이유는 나 또한 조만간 이력서를 수정해야겠다는 생각을 하였다.
  • 왜냐하면 이력서를 수정하고 언 2개월이 지났는데 내가 또 무엇이 부족한지 어떠한 내용을 수정해야 하는지 확인하기 위해서 수정해야겠다는 생각을 하였다.

2023-07-06 스터디

  • 오늘은 영한 님의 인강을 들으며 출근하였다.
  • 개발바닥의 영상을 보면서 느낀 거지만 나 또한 너무 옆으로 넓고 얕게 지식을 쌓으려는 경향이 강했던거 같다.
  • 자바와 스프링에 대해 깊게 공부해야겠다는 생각을 하였다.

스터디 BOJ_10871

문제

  • 문제
    • 아직 글을 모르는 영석이가 벽에 걸린 칠판에 자석이 붙어있는 글자들을 붙이는 장난감을 가지고 놀고 있다.
    • 이 장난감에 있는 글자들은 영어 대문자 ‘A’부터 ‘Z’, 영어 소문자 ‘a’부터 ‘z’, 숫자 ‘0’부터 ‘9’이다. 영석이는 칠판에 글자들을 수평으로 일렬로 붙여서 단어를 만든다. 다시 그 아래쪽에 글자들을 붙여서 또 다른 단어를 만든다. 이런 식으로 다섯 개의 단어를 만든다. 아래 그림 1은 영석이가 칠판에 붙여 만든 단어들의 예이다.
      A A B C D D
      a f z z 
      0 9 1 2 1
      a 8 E W g 6
      P 5 h 3 k x
      

      <그림 1>

    • 한 줄의 단어는 글자들을 빈칸 없이 연속으로 나열해서 최대 15개의 글자들로 이루어진다. 또한 만들어진 다섯 개의 단어들의 글자 개수는 서로 다를 수 있다.
    • 심심해진 영석이는 칠판에 만들어진 다섯 개의 단어를 세로로 읽으려 한다. 세로로 읽을 때, 각 단어의 첫 번째 글자들을 위에서 아래로 세로로 읽는다. 다음에 두 번째 글자들을 세로로 읽는다. 이런 식으로 왼쪽에서 오른쪽으로 한 자리씩 이동 하면서 동일한 자리의 글자들을 세로로 읽어 나간다. 위의 그림 1의 다섯 번째 자리를 보면 두 번째 줄의 다섯 번째 자리의 글자는 없다. 이런 경우처럼 세로로 읽을 때 해당 자리의 글자가 없으면, 읽지 않고 그 다음 글자를 계속 읽는다. 그림 1의 다섯 번째 자리를 세로로 읽으면 D1gk로 읽는다.
    • 그림 1에서 영석이가 세로로 읽은 순서대로 글자들을 공백 없이 출력하면 다음과 같다:
    • Aa0aPAf985Bz1EhCz2W3D1gkD6x
    • 칠판에 붙여진 단어들이 주어질 때, 영석이가 세로로 읽은 순서대로 글자들을 출력하는 프로그램을 작성하시오.
  • 입력
    • 총 다섯줄의 입력이 주어진다. 각 줄에는 최소 1개, 최대 15개의 글자들이 빈칸 없이 연속으로 주어진다. 주어지는 글자는 영어 대문자 ‘A’부터 ‘Z’, 영어 소문자 ‘a’부터 ‘z’, 숫자 ‘0’부터 ‘9’ 중 하나이다. 각 줄의 시작과 마지막에 빈칸은 없다.
      • 출력
        • 영석이가 세로로 읽은 순서대로 글자들을 출력한다. 이때, 글자들을 공백 없이 연속해서 출력한다.
      • 입력 예제1        출력 예제 1
            
        ABCDE           Aa0FfBb1GgCc2HhDd3IiEe4Jj
        abcde
        01234
        FGHIJ
        fghij
            
        입력 예제1        출력 예제 1
        AABCDD          Aa0aPAf985Bz1EhCz2W3D1gkD6x
        afzz
        09121
        a8EWg6
        P5h3kx
        

풀이

// file: 풀이.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
  public static void main(String[] args) throws IOException {
    
    // 1) 첫 번째 방법
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    char[][] words = new char[5][15]; // 문자 하나씩 저장하기 위한 이중 배열 선언

    // 다섯 줄의 단어(word)를 입력 받아 words 배열에 담음
    for (int i = 0; i < 5; i++) { // 5번 반복
      String word = br.readLine();
      for (int j = 0; j < word.length(); j++) { // 입력한 값의 개수만큼 반복
        // charAt() 해당 인덱스의 값을 반환해줌
        words[i][j] = word.charAt(j); // words i번째 j번 방에 입력한 문자열 word 변수의 j번째 값 넣기
      }
    }

    // 세로로 읽어들인 문자들을 출력함
    // String이 아닌 StringBuilder를 사용한 이유?
    // String의 경우 기존에 선언한 값에 추가(+=)한 경우 새로운 객체로 탄생하게 된다.
    // 하지만, StringBuilder의 경우 sb.append("xxx") 새로운 값을 추가하게 되면 기존 StringBuilder의 객체에 새로운 값이 추가된다.
    StringBuilder sb = new StringBuilder();
    for (int j = 0; j < 15; j++) {
      for (int i = 0; i < 5; i++) {
        if (words[i][j] != '\0') { // \0은 널 문자(Null character)를 나타낸다. 즉, null이 아닐때
          sb.append(words[i][j]); // 횡(row)로 append해주기
        }
      }
    }
    System.out.println(sb);
    
    // -------------------------------------------------------------

    // 2) 두 번째 Stream API를 이용한 방법
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // BufferedReader를 사용하여 입력을 처리
    String[] words = new String[5]; // 문자열 배열 words를 선언

    for (int i = 0; i < 5; i++) { // 반복문을 사용하여 5개의 문자열을 입력 받고, words 배열에 저장
      words[i] = br.readLine();
    }

    String result = IntStream.range(0, 15) // IntStream을 사용하여 0부터 14까지의 범위를 생성. 이 숫자들은 문자열의 인덱스를 나타낸다.
            // .mapToObj()를 사용하여 각 인덱스에 대해 아래의 연산을 수행
            .mapToObj(j -> IntStream.range(0, 5) // 내부적으로 또 다른 IntStream을 생성하여 0부터 4까지의 범위를 나타낸다. 이 숫자들은 각 문자열의 인덱스이다
                    .filter(i -> j < words[i].length()) // .filter()를 사용하여 해당 인덱스에 문자가 있는지 확인하고, 필요한 경우 해당 문자열에서 문자를 가져온다.
                    .mapToObj(i -> words[i].charAt(j)) // .mapToObj()를 사용하여 문자를 문자 객체로 변환
                    .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString()) // .collect()를 사용하여 내부 인덱스(IntStream의 요소)에 대해 필터링된 문자를 차례대로 StringBuilder 객체에 추가
            .collect(Collectors.joining()); // .collect(Collectors.joining())을 사용하여 각 인덱스의 문자들을 모아 하나의 문자열로 만든다.

    System.out.println(result);
  }
}

2023-07-07

  • 오늘도 어김없이 영한 님의 인강과 함께

Back to [Routine] 24 주차 시작!

Continue with [Routine] 26 주차 시작!

[JUnit5] JUnit5 어노테이션 ver1

[JUnit5] JUnit5 어노테이션 ver1

#목차

JUnit5 Annotations ver1

@Test

  • 테스트 메서드라는 것을 나타내는 어노테이션
  • JUnit4와 다르게 어떠한 속성도 선언하지 않는다.
  • 또한, 클래스와 메서드는 더 이상 가시성(visibility)이 public으로 선언되지 않아도 동작한다.(물론 기존과 같이 public, protected등으로 선언해도 동일하게 동작한다.)
class JUnitTest() {
    // JUnit4
    @Test(exptected = Exception.class)
    public void 상품주문() throws Exception {
        // ...
    }
    
    // JUnit5
    @Test
    void 상품주문() {
        // ...
    }
}

@Test 결과

@Test Annotation

@DisplayName

  • Test Results에 나오는 테스트 클래스, 메소드 이름을 정할 수 있는 어노테이션.
파라미터명타입설명
valueString테스트명 (한글, 특수문자, 이모지 등) 다양한 문자 가능
public class DisplayNameTest {
    
    @Test
    void 기본() {
    }
    
    @DisplayName("한글 메서드")
    @Test
    void 한글() {
    }
    
    @DisplayName("english")
    @Test
    void english() {
    }
    
    @DisplayName("특수문자 \uD83D\uDE00")
    @Test
    void 특수문자() {
    }
}

@DisplayName 결과

@DisplayName Annotation

@DisplayNameGeneration

  • @DisplayName 처럼 별도의 이름을 주는 것이 아닌 코딩한 클래스, 메소드 이름을 이용해 변형시키는 어노테이션
파라미터명타입설명
valueClass<? extends DisplayNameGenerator>정의된 DisplayNameGenerator 중 하나 사용
  • 위 파라미터 값은 DisplayNameGenerator 클래스에 사용할 수 있는 방법이 내부 클래스로 정의되어있다.
클래스명설명
Standard기존 클래스, 메소드 명을 사용 (기본값)
Simple괄호()제외
ReplaceUnderscores_(underscore) 를 공백으로 바꿈
IndicativeSentences클래스명 + 구분자(“, “) + 메소드명으로 바꿈.
(@IndicativeSentencesGeneration를 이용해서 구분자를 커스텀하게 변경할 수 있다
public class DisplayNameGenerationTest {
    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class) // inner class, 메서드명 출력
    class IndicativeSentences {
        @Test
        void test_display_name_generation() {
        }
    }

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) // _(underscore) 를 공백으로 바꿈
    class ReplaceUnderscores {
        @Test
        void test_display_name_generation() {
        }
    }

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.Simple.class) // 괄호()를 제외 
    class Simple {
        @Test
        void test_display_name_generation() {
        }
    }

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.Standard.class) // 기본값. 메서드 이름 그대로 출력
    class Standard {
        @Test
        void test_display_name_generation() {
        }
    }
}

@DisplayNameGeneration 결과

@DisplayNameGeneration Annotation

@ParameterizedTest

  • 파라미터를 넣어서 데스트를 반복적으로 실행할 수 있게 해주는 어노테이션.
  • 반드시 파라미터를 넘겨야하며, @ValueSource 혹은 @CsvSource 등을 붙여서 파라미터를 같이 던져주어야한다.

@ValueSource

  • 여러 번의 파라미터를 던질 수 있도록 하는 어노테이션 중 하나이다. 단 하나의 타입만 던질 수 있으며 사용법은 아래와 같다.
  • @ValueSource(ints = {10, 20, 30}) [O] , @ValueSource(ints = {10, 20, 30}, strings = {"문자1", "문자2", "문자3") [X]
  • 단 하나의 타입의 파라미터만 가능하므로 두 번째 케이스는 에러가 날 것이다.
class ParameterizedTestTest {

    private Set<Integer> numbers;

    @BeforeEach
    void setup() {
        numbers = new HashSet<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
    }

    @DisplayName("일반_반복테스트")
    @Test
    void 일반_반복테스트() {
        assertThat(numbers.contains(1)).isTrue();
        assertThat(numbers.contains(2)).isTrue();
        assertThat(numbers.contains(3)).isTrue();
        for (int num : numbers) {
            System.out.println("일반 반복 테스트 : " + num);
        }
    }

    @DisplayName("반복_테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    void 반복_테스트(int intArg) {
        assertThat(numbers.contains(intArg)).isTrue();
    }
}

일반 반복 테스트 결과

일반 반복 테스트

@ValueSource 반복 테스트 결과

@ValueSource 반복 테스트

@CsvSource

  • ,로 분리된 문자열을 테스트 메서드의 파라미터로 넣어주는 어노테이션이다.
  • delimiterString이라는 옵션으로 문자열을 분리할 다른 구분자를 고를 수 있다.
  • ' '로 넣는 것은 empty가 들어오고 아에 비워두면 null로 들어오게 된다.
public class CvsSourceTest {
    @ParameterizedTest
    @CsvSource({ "apple, banana" })
    void cvsSourceTest1(String fruit1, String fruit2) {
    }

    @ParameterizedTest
    @CsvSource({ "apple, 'lemon, lime'" })
    void cvsSourceTest2(String fruit1, String fruit2) {
    }

    @ParameterizedTest
    @CsvSource({ "apple, ''" })
    void cvsSourceTest3(String fruit1, String fruit2) {
    }

    @ParameterizedTest
    @CsvSource({ "apple, " })
    void cvsSourceTest4(String fruit1, String fruit2) {
    }

    @ParameterizedTest
    @CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL")
    void cvsSourceTest5(String fruit1, String fruit2) {
    }
}

@CsvSource 테스트 결과

@CsvSource 테스트

@RepeatedTest

  • @RepeatedTest메서드를 어노테이션을 지정하면 value값에 지정된 횟수만큼 테스트가 반복된다.
public class RepeatTest {
    @RepeatedTest(5)
    @DisplayName("반복 테스트1")
    void repeatedTest1() {
    }

    @RepeatedTest(value = 5, name = "반복테스트2 중 {currentRepetition} of {totalRepetitions}")
    @DisplayName("반복 테스트2")
    void repeatedTest2() {
    }
}

@RepeatedTest 테스트 결과

@RepeatedTest 테스트

@BeforeEach

  • @BeforeEach 어노테이션이 달린 메서드가 현재 클래스의 각각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 어노테이션이 달린 메서드 전에 실행된다.

@AfterEach

  • @AfterEach 어노테이션이 달린 메서드가 현재 클래스의 각각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 어노테이션이 달린 메서드 이후에 실행된다.

@BeforeAll

  • @BeforeAll 어노테이션이 달린 메서드가 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest@TestFactory 어노테이션이 달린 메서드보다 먼저 실행된다.

@AfterAll

  • @AfterAll 어노테이션이 달린 메서드가 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 어노테이션이 달린 메서드 다음에 실행된다.

@BeforeAll vs @BeforeEach 와 @AfterAll vs @AfterEach

  • 두 개 어노테이션 모두 하나 혹은 여러 개의 테스트 조건을 setup할 때 사용하지만 하나의 테스트 클래스 안에 여러 개의 테스트가 있는 경우 @BeforeEach@AfterEach는 여러번 실행되지만, @BeforeAll@AfterAll은 딱 한 번만 실행된다.
  • @BeforeEach@AfterEach@Test 어노테이션이 붙은 메서드가 실행이 되기 전(@BeforeEach)과 후(@AfterEach) 실행이 된다.
  • @BeforeAll@AfterAll은 해당 클래스 내에 처음 시작(@BeforeAll)과 맨 마지막 끝(@AfterAll)에 실행이 된다.

JUnit5 LifeCycle

class JUnit5LifeCycleTest {

    @BeforeAll
    static void setup() {
        System.out.println("@BeforeAll - executes once before all test methods in this class");
    }
  
    @BeforeEach
    void init() {
        System.out.println("@BeforeEach - executes before each test method in this class");
    }
  
    @AfterEach
    void tearDown() {
        System.out.println("@AfterEach - executed after each test method.");
    }
  
    @AfterAll
    static void tearDownAll() {
        System.out.println("@AfterAll - executed after all test methods.");
    }
  
    @Test
    void successTest1() {
        System.out.println("executes successTest1");
        assumeTrue("abc".contains("a"));
    }
  
    @Test
    void successTest2() {
        System.out.println("executes successTest2");
        assumeTrue("abc".contains("b"));
    }
    
    // 출력 ---------------------------------------------------------
    /*
      @BeforeAll - executes once before all test methods in this class
      @BeforeEach - executes before each test method in this class
      executes successTest1
      @AfterEach - executed after each test method.
      @BeforeEach - executes before each test method in this class
      executes successTest2
      @AfterEach - executed after each test method.
      @AfterAll - executed after all test methods.
    */
}

결과 출력

JUnit5 LifeCycle Example 1

Continue with [JUnit5] JUnit5 어노테이션2

Back to [JUnit5] JUnit5

Reference

[Routine] 24 주차 시작!

[Routine] 24 주차 시작!

2023년 06월 26일 부터 06월 30일 까지의 나의 루틴.

#목차

24주차

2023-06-26

  • 오늘도 어김없이 영한 님의 인강과 바킹독 님의 코테 인강과 함께 출근하였다.

코테 커밋

  • [BOJ_2309_일곱난쟁이]{:target=”_blank”}

2023-06-27

  • 오늘도 어김없이 영한 님의 인강과 함께!

2023-06-28

  • 역시나 오늘도 영한 님의 인강과 바킹독 님의 코테 인강을 들으며 출근하였다.

2023-06-29 스터디

  • 오늘은 바킹독 님의 코테 인강과 함께 출근하였다.

코테 커밋


스터디 BOJ_10871

문제

  • 문제
    • 정수 N개로 이루어진 수열 A와 정수 X가 주어진다. 이때, A에서 X보다 작은 수를 모두 출력하는 프로그램을 작성하시오.
  • 입력 1) 첫째 줄에 N과 X가 주어진다. (1 ≤ N, X ≤ 10,000) 2) 둘째 줄에 수열 A를 이루는 정수 N개가 주어진다. 주어지는 정수는 모두 1보다 크거나 같고, 10,000보다 작거나 같은 정수이다.
  • 출력
    • X보다 작은 수를 입력받은 순서대로 공백으로 구분해 출력한다. X보다 작은 수는 적어도 하나 존재한다.

풀이

// file: 풀이.java
import java.util.Scanner;

public class Main {
  public static void main(String[] args) throws IOException {
    Scanner sc = new Scanner(System.in);

    int N = sc.nextInt();   // 정수 N
    int X = sc.nextInt();   // 정수 X

    int[] A = new int[N];   // 정수 N으로 이루어진 수열(배열) A

    // 정수 N개로 이루어진 수열 A의 조건을 만족하기 위한 반복문
    for (int i = 0; i < N; i++) {
      A[i] = sc.nextInt();
      if (A[i] < X) {
        System.out.print(A[i] + " ");
      }
    }
    sc.close(); // 스캐너 close
  }
}

2023-06-30

  • 오늘도 어김없이 영한 님의 인강을 들으며 출근 하였다.

Back to [Routine] 23 주차 시작!

Continue with [Routine] 25 주차 시작!

Pagination