단위 테스트
모듈 하나 단위의 테스트라고 생각하면 된다.
예를 들어 보겠다.
User라는 객체를 활용하려 클래스를 만들었는데,
실제로 우리가 이 객체를 활용해서 서버에서의 활용할 수 있는지 알아야한다.
1. Edge Case 파악
위에서 언급한 것과 같은 맥락인데, 활용 가능하다는 것의 정의는,
지극히 개인적인 견해이지만, 단위테스트에서는
- Null 값이 들어오지 않아야하고,
- 값이 있어도 유효한 값이어야 한다. (비즈니스 로직에 부합해야 한다.)
따라서 Edge Case는 위의 2가지로 일단 정의내려보았다.
왜 비즈니스 로직을 서비스에서 처리하지 않았나? 라는 의문이 들 수 있는데,
'테스트에 용이하지 않아서다.'
통합테스트는 기본적으로 모듈 2개 이상을 사용한다.
통합 테스트를 시도하다보면, 비즈니스 로직과 DB에서 데이터를 잘 넣고 가져오는지 까지 모두 함께 테스트하려니
'어디에서부터 테스트범위인지' 애매해진다.
테스트하기 용이한 코드를 선호한다면 (Test Driven Development),
서비스 단에서는 실제로 DB에 왔다가 오는 것만을 처리하는 것이 테스트 코드를 작성하기에 용이하다. (개인적인 견해다!!)
따라서 나는 단위 테스트에서는 'DB'에 갔다오기 전에 체크할 수 있는 것을 체크하며,
통합 테스트에서는 DB에 갔다오는 것을 체크한다.
class UserTest {
// 테스트에 계속 쓰이는 변수 선언
Long userId;
String username = "test@naver.com";
String password = "1234";
String nickName = "테스트용 유저";
@Nested
@DisplayName("회원 로그인 테스트")
//로그인 관련 전체 테스트를 담는 클래스 : 정상과 실패 케이스로 나뉜다.
class UserLoginTest {
@Test
@DisplayName("정상 케이스")
void LoginUser_Normal() {
UserLoginDto userLoginDto = new UserLoginDto();
userLoginDto.setUsername(username);
userLoginDto.setPassword(password);
User user = new User();
// when
String validationCheck = user.isValidLogin(userLoginDto);
// then
assertThat(validationCheck, containsString("success"));
}
// 실패 케이스는 다시 하위 분류로 나뉜다.
@Nested
@DisplayName("실패 케이스")
class LoginUser_Fail{
@Nested
@DisplayName("회원 아이디")
class UserId {
@Test
@DisplayName("null이거나 빈 문자열")
void fail1() {
// when
UserLoginDto userLoginDto = new UserLoginDto();
// 일부러 아이디를 넣지 않았다.
userLoginDto.setPassword(password);
User user = new User();
String validationCheck = user.isValidLogin(userLoginDto);
// then
// 잘 체크했다면, 아이디 값이 비었을 때 관련 처리가 되어야한다.
assertThat(validationCheck, containsString("아이디는 필수 입력입니다."));
}
}
@Nested
@DisplayName("회원 비밀번호")
class Password {
@Test
@DisplayName("null이거나 빈 문자열")
void fail1() {
// when
UserLoginDto userLoginDto = new UserLoginDto();
// 역시 일부러 비밀번호를 넣지 않았다.
userLoginDto.setUsername(username);
User user = new User();
String validationCheck = user.isValidLogin(userLoginDto);
// then
// 예외를 잘 처리했다면, 비밀번호가 없는 것에 대해서는 관련 메세지가 나와야한다.
assertThat(validationCheck, containsString("비밀번호는 필수 입력입니다."));
}
}
}
}
}
위의 코드의 핵심은 다음과 같다.
- 로그인 테스트는 크게 2가지로 나뉜다 : 정상과 실패 케이스
- 실패 케이스는 다시 하위로 나뉜다 : 아이디값 무효, 비밀번호값 무효
다른 객체의 단위테스트도 이 부분을 활용하면 될 것 같다.
그렇다면 실제 User 객체 내부는 어떻게 유효성을 체크하고 있는지 알아보자.
package com.sparta.juteukki02.juteukki_week02.model;
import com.sparta.juteukki02.juteukki_week02.Dto.UserLoginDto;
import com.sparta.juteukki02.juteukki_week02.Dto.UserRegisterDto;
import com.sparta.juteukki02.juteukki_week02.util.Helper;
import lombok.*;
import javax.persistence.*;
import java.util.regex.Pattern;
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
@AllArgsConstructor
@Builder
public class User {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String nickName;
//유효성 체크를 모델에서 하는 이유는, 이후에 단위 테스트에 용이하기 때문
// 로그인 시에 넘겨받은 값이 유효한지 체크하는 함수
public String isValidLogin(UserLoginDto userLoginDto){
String username = userLoginDto.getUsername();
String password = userLoginDto.getPassword();
Helper.JSONBuilder builder = new Helper.JSONBuilder();
// 아이디 값 체크 : Null, 유효 여부
if(username == null || username.isEmpty()){
builder.addKeyValue("result", "fail");
builder.addKeyValue("msg", "아이디는 필수 입력입니다.");
return builder.build().getReturnJSON();
}
// 비밀번호 값 체크 : Null, 유효 여부
else if(password == null || password.isEmpty()){
builder.addKeyValue("result", "fail");
builder.addKeyValue("msg", "비밀번호는 필수 입력입니다.");
return builder.build().getReturnJSON();
}
return "success";
}
// 회원가입 시에 넘겨받은 값이 유효한지 체크하는 함수
public String isValidRegister(UserRegisterDto userRegisterDto){
String username = userRegisterDto.getUsername();
String password = userRegisterDto.getPassword();
String passwordCheck = userRegisterDto.getPasswordCheck();
String nickName = userRegisterDto.getNickName();
Helper.JSONBuilder builder = new Helper.JSONBuilder();
builder.addKeyValue("result", "fail");
// Null과 빈 문자열 체크
if(username == null || username.isEmpty()){
builder.addKeyValue("msg", "아이디는 필수 입력입니다.");
return builder.build().getReturnJSON();
}
else if(password == null || password.isEmpty()){
builder.addKeyValue("msg", "비밀번호는 필수 입력입니다.");
return builder.build().getReturnJSON();
}
else if(passwordCheck == null || passwordCheck.isEmpty()){
builder.addKeyValue("msg", "비밀번호 확인은 필수 입력입니다.");
return builder.build().getReturnJSON();
}
else if(nickName == null || nickName.isEmpty()){
builder.addKeyValue("msg", "별명은 필수 입력입니다.");
return builder.build().getReturnJSON();
}
// 비즈니스 로직 적용
// - 닉네임은 `최소 3자 이상, 알파벳 대소문자(a~z, A~Z), 숫자(0~9)`로 구성하기
if(!Pattern.matches("^[A-Za-z0-9]*$", nickName) || nickName.length() < 3){
builder.addKeyValue("msg", "닉네임은 최소 3자 이상, 알파벳 대소문자(a~z, A~Z), 숫자(0~9)입니다.");
return builder.build().getReturnJSON();
}
// - 비밀번호는 `최소 4자 이상이며, 닉네임과 같은 값이 포함된 경우 회원가입에 실패`로 만들기
if(password.contains(nickName) || password.length() < 4){
builder.addKeyValue("msg", "비밀번호는 `최소 4자 이상이며, 닉네임과 같은 값이 포함될 수 없습니다.");
return builder.build().getReturnJSON();
}
// - 비밀번호 확인은 비밀번호와 정확하게 일치하기
if(!password.equals(passwordCheck)){
builder.addKeyValue("msg", "비밀번호 일치 여부를 확인해주세요.");
return builder.build().getReturnJSON();
}
return "success";
}
}
위에서 중요 함수는 isValidLogin, isValidRegister이다.
Null 값과 비즈니스 로직을 적용시킨 것이다.
(참고로 builder 패턴을 적용해서 JSON을 리턴하는 것은 정해진 리턴 값을 만들어서 리턴하기 위함입니다.
실제로 프로젝트 진행시, 팀원들과 어떤 JSON 형태를 리턴할지 약속했기 때문입니다.
직접 커스텀한 클래스의 함수입니다!! 내장 함수 아니니, 그대로 쓰시면 안됩니다.
예외처리시의 메세지는 각자의 원하는 리턴 방식을 적용하시면 됩니다.)
이제 통합테스트로 넘어가보겠다.
통합 테스트
통합테스트는 모듈 2개 이상일 떄 하는 테스트라고 생각하면 될 것 같다.
혹시 만약 테스트를 하기 전에, 어디서부터 어디까지 테스트 범위로 잡아야할지 애매하다면,
실제로 단위와 통합 테스트를 나눌 때, 나는
DB에 갔다오느냐 아니냐를 기준으로 분류해보았다.
package com.sparta.juteukki02.juteukki_week02.integration;
import com.sparta.juteukki02.juteukki_week02.Dto.UserLoginDto;
import com.sparta.juteukki02.juteukki_week02.model.User;
import com.sparta.juteukki02.juteukki_week02.model.UserRepository;
import com.sparta.juteukki02.juteukki_week02.service.UserService;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.jupiter.api.Assertions.*;
// 실제 스프링을 구동시킨다는 뜻
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 테스트 객체라는 뜻, 테스트 주기를 설정
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserIntegrationTest {
@Autowired
UserService userService;
@Autowired
UserRepository userRepository;
//계속 쓸 테스트용 변수 선언
Long id = 100L;
String username = "abcd";
String password = "1234";
String passwordcheck = "1234";
String nickName = "Cheese";
@Test
@DisplayName("회원가입시 중복된 아이디, 닉네임 체크")
void test1() {
// given
User user = new User();
// 여기서는 username이 아이디라고 생각하면 된다. 이미 존재하는 아이디를 일부러 넣어보았다.
user.setUsername(username);
user.setPassword(password);
user.setPasswordCheck(passwordcheck);
user.setNickName(nickName);
// when
String result = userService.checkRegister(user);
// then
assertThat(result).doesNotContain("중복된 사용자 ID 가 존재합니다.");
assertThat(result).doesNotContain("중복된 사용자 닉네임이 존재합니다.");
// }
}
이번에는 회원가입을 기준으로 체크해보았다.
회원가입시에, 중복된 사용자 ID나, 닉네임은 DB에서 확인을 해보아야한다.
일부러 실제 DB에 있는 아이디 (username)을 넣어보고,
올바르게 메세지를 리턴하는지 체크해보았다.
package com.sparta.juteukki02.juteukki_week02.service;
import com.sparta.juteukki02.juteukki_week02.Dto.UserLoginDto;
import com.sparta.juteukki02.juteukki_week02.Dto.UserRegisterDto;
import com.sparta.juteukki02.juteukki_week02.jwt.JwtTokenProvider;
import com.sparta.juteukki02.juteukki_week02.model.User;
import com.sparta.juteukki02.juteukki_week02.model.UserRepository;
import com.sparta.juteukki02.juteukki_week02.util.Helper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
//회원가입 유효한지 체크.
public String checkRegister(UserRegisterDto userRegisterDto){
String username = userRegisterDto.getUsername();
String password = userRegisterDto.getPassword();
String nickName = userRegisterDto.getNickName();
// 회원 ID 중복 확인
Optional<User> foundId = userRepository.findByUsername(username);
if (foundId.isPresent()) {
Helper.JSONBuilder builder = new Helper.JSONBuilder();
builder.addKeyValue("result", "fail");
builder.addKeyValue("msg", "중복된 사용자 ID 가 존재합니다.");
return builder.build().getReturnJSON();
}
// 회원 이름 중복 확인
Optional<User> foundName = userRepository.findByNickName(nickName);
if (foundName.isPresent()) {
Helper.JSONBuilder builder = new Helper.JSONBuilder();
builder.addKeyValue("result", "fail");
builder.addKeyValue("msg", "중복된 사용자 닉네임이 존재합니다.");
return builder.build().getReturnJSON();
}
// 빌더 패턴 적용, 회원 정보 저장
User user = User.builder()
.username(userRegisterDto.getUsername())
// 패스워드 암호화
.password(passwordEncoder.encode(password))
.nickName(userRegisterDto.getNickName())
.build();
userRepository.save(user);
Helper.JSONBuilder builder = new Helper.JSONBuilder();
builder.addKeyValue("result", "success");
builder.addKeyValue("msg", "회원가입에 성공하였습니다.");
return builder.build().getReturnJSON();
}
}
실제 서비스 단에서 회원가입을 체크하는 것은 저렇다.
결론
- 단위와 통합은 모듈이 1개냐, 그 이상이냐로 나눌 수 있고,
흔히들 객체와 서비스단에서 체크를 많이한다.
- 만약 테스트를 나누는 기준이 애매하다면, DB에 붙는지 안 붙는지를 따져보는 것도 좋은 방법이다.
'백엔드 > 테스팅' 카테고리의 다른 글
기능 테스팅 (0) | 2022.10.14 |
---|---|
소프트웨어 테스팅의 기초 (0) | 2022.08.31 |
성능 테스트 - Ngrinder (0) | 2022.03.07 |