#목차
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 Connect 는 OAuth 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)는 공격자가 웹 페이지에 악성 스크립트를 삽입하여 다른 사용자의 데이터를 탈취하거나 조작하는 공격이다.
- 스프링 시큐리티는 사용자의 입력을 필터링하거나 이스케이프하여 이러한 공격을 방어한다.
이스케이프 처리란?
& => &
, ' => '
, " => "
, < => <
, > => >
, / => /
즉, 코드로써 작성될 수 있는 기호들을 이스케이프 처리한 텍스트로 작성하는 것이다.
- 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
#목차
MySQL group_concat
JPA QueryDSL
에서 group_concat()
함수 사용법
MySQL group_concat 함수 란?
GROUP_CONCAT
은 MySQL
에서 제공하는 집계 함수 중 하나로, 선택한 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에 종속된 함수(예, MySQL
의 group_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));
}
}
주의!

- 위 사진처럼
MySQL5InnoDBDialect
와 MySQL57InnoDBDialect
이 deprecated(사용하지 않는)
되어있다. - 그래서 난 위 코드처럼
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
에서도 MySQL
의 group_concat()
함수를 사용할 수 있다.
QueryDSL에 group_concat() 적용 쿼리문 확인


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

- 정상적으로 같은 컬럼의 각기 다른 값들을 한 row에 문자열 방식으로 출력 되었다.
정리
Hibernate
는 특정 데이터베이스에 종속되지 않고 객체지향
스럽게 사용할 수 있도록 추상화
해주어 특정 DB에 종속된 함수(예, MySQL
의 group_concat
)는 제공하지 않는다.- 때문에 JPA QueryDSL에서 특정 DB의 함수를 사용하려면
설정
해주어야 한다. group_concat()
을 사용할 땐 꼭 GROUP BY 컬럼명
을 이용하여 한 줄(row)에 출력할 수 있게 해야한다.
“읽은 기간 : 2023-08-18 ~ 2023-09”
아메리칸 프로메테우스
책 정보
- 제목 : 아메리칸 프로메테우스
- 부제 : 오펜하이머 평전
- 저자 : 카이 버드, 마틴 셔원
- 쪽수 : 1056쪽
- 발행(출시)일자 : 2023년 06월 12일
- ISBN : 9791192908236
#목차
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()
메서드의 파라미터로 필드명
을 입력하거나, 메서드 레퍼런스
도 표현이 가능하다.
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()
는 순서 원소의 개수와 값이 모두 일치해야 한다.
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 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
를 사용한다.JUniit5
와 Mockito
를 연동해 테스트를 진행할 경우에는 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.Path
나 java.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 Test
는 Runtime
동안에 테스트가 생성되고 수행되기 때문에, 프로그램이 수행되는 도중에도 동작을 변경할 수 있는 특징이 있다.
@TestFactory
Continue with [JUnit5] Dynamic Test @TestFactory
@TestTemplate
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 Annotations
Dynamic Test
‘Dynamic Test`는 Runtime 시점에 생성’
- 일반적으로 사용되는
@Test
를 이용한 테스트는 컴파일 시점에 완전히 지정되는 정적 테스트. 이는 동적인 기능에 대한 기본 테스트 형태를 제공하지만, 그 표현이 컴파일 시점에 제한된다는 한계를 가지고 있다. - 이에 비해
Dynamic Test
는 Runtime
동안에 테스트가 생성되고 수행되기 때문에, 프로그램이 수행되는 도중에도 동작을 변경할 수 있는 특징이 있다.
@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
“2023년 07월 10일 부터 07월 14 까지의 나의 루틴.”
#목차
26주차
2023-07-10
- 오늘도 어김없이 영한 님의 인강과 함께 출근하였다.
2023-07-11
2023-07-12
- 앞으로 루틴에 대해 더 이상 포스팅 하지 않고 깃 커밋으로 대체하려 한다.
- 또한 루틴 포스트에서 작성한 내용들을 깃으로 다 넘기려 한다.
- 블로그 루틴 포스팅을 지우고 한달에 한 번 정도의 자료 조사를 통해 블로그 포스팅을 하려한다.
Back to [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 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
에 나오는 테스트 클래스, 메소드 이름을 정할 수 있는 어노테이션.
파라미터명 | 타입 | 설명 |
---|
value | String | 테스트명 (한글, 특수문자, 이모지 등) 다양한 문자 가능 |
public class DisplayNameTest {
@Test
void 기본() {
}
@DisplayName("한글 메서드")
@Test
void 한글() {
}
@DisplayName("english")
@Test
void english() {
}
@DisplayName("특수문자 \uD83D\uDE00")
@Test
void 특수문자() {
}
}
@DisplayName 결과
@DisplayName Annotation
@DisplayNameGeneration
@DisplayName
처럼 별도의 이름을 주는 것이 아닌 코딩한 클래스, 메소드 이름을 이용해 변형시키는 어노테이션
파라미터명 | 타입 | 설명 |
---|
value | Class<? 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
“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 주차 시작!