4장 테스트 구축

600g (Kim Dong Geun)·2021년 12월 11일
0

4장 테스트 구축하기

1. 자가 테스트 코드의 장점
  • 코드의 검증을 컴파일러가 수행할 수 있도록 한다.
    • 결과를 정의해두고, 정의해둔대로 코드가 결과대로 실행했는지는 테스트 코드가 검증해준다.
  • 따라서 사람이 하는 실수를 원천 방지 -> 생산성 증대
  • 테스트 프레임워크 JUnit, Mocha
  • TDD 기법의 탄생
    • 설계 - 테스트 코드 작성 - 구현 - 결과 확인의 과정
    • TDD가 대략 어떻게 이뤄지는지 보여주자.

책 내용대로 가기에는 책 내용이 너무 함축적인 내용만 담고 있어서 실제적인 단물쪽쪽은 거의 없음.

책에서는 리팩토링 하는 코드가 제대로 돌아가는지 검증하는 용도로 쓰이는 것 같음.

테스트 코드 작성하기

어떤 내용이 좋을까 생각해서 다음의 경우를 생각해봄

  • 책내용에서는 Mocha, 나는 Junit5를 이용해서 테스트

  • 회원가입 로직 작성해보기

    • TDD
    • 데이터 경계값 조사 혹은 데이터 정합성이 중요한 경우 (에러가 많이 나오는 경우)
    • 테스트 변경
    • 통합 테스트
  • 회원가입에 필요한 로직이 몇가지가 있을까라고 생각했을때
    • 그럼 TDD로 코드 부터 짜보자.
    • 조건은 코드를 작성해나가면서 생각하면서
  • 우선 회원가입시 어떤 정보를 받을지 생각해보자
    • id, password, name 등을 받을꺼고
    • 패스워드는 영문, 숫자 혼합에 6글자 이상
    • 그리고 이름은 한글로만 작성이 가능하다.
    • 라는 조건을 걸면서 Entity를 작성해보자
@Data
public class UserEntity {
    private int seq;
    private String id;
    private String password;
    private String name;
}
  • 당연히 DB 연결은 굉장히 귀찮은작업 이니 있다고만 생각하고 가정하자
public interface UserRepository {
    boolean existUserById(String userId);

    UserEntity save(UserEntity userEntity);
}
  • 그럼 이제 TDD 기본인 회원가입을 하는 테스트 코드로 설계를 해보자

    • 우선 회원가입을 할꺼다. UserService를 만들자.
    • 우린 회원가입을 만들꺼다
    public interface UserService {
        void addUser(UserEntity.VO userVO);
    }
    
    @Service
    public class UserServiceImpl implements UserService{
        @Override
        public void addUser(UserEntity.VO userVO) {
            
        }
    }
    
    • 회원가입을 하기위해서는 뭐가 필요하냐
      1. param 체크
        1. 패스워드는 영문, 숫자 혼합에 6글자 이상
        2. 그리고 이름은 한글로만 작성이 가능하다. 한글뺄래
      2. DB 동작
        1. 아이디 중복체크여부
        2. 아이디가 DB에 제대로 들어갔는지
  • Fixture를 설정하자.

    userId : test1
    password : test1234
    name : 테스트유저
  • 테스트 코드를 작성하자

  • @SpringBootTest
    public class UserServiceTest {
    
        @InjectMocks
        private final UserService userService = new UserServiceImpl();
    
        private UserEntity.VO testUser;
    
        @BeforeEach
        public void setUp(){
            UserEntity.VO.builder()
                    .id("test1")
                    .password("test1234")
                    .name("테스트 유저");
        }
    
        @Test
        public void testAddUser(){
            userService.addUser(testUser);
        }
    }
    
  • Param 체크가 올바로 되어야한다. (실패난 테스트 생각해서 짜기)

//경계값 혹은 정합성에 어긋난 실패난 경우
@Service
public class UserServiceImpl implements UserService {

    private final Pattern pattern = Pattern.compile("[a-z|A-Z]{1}[a-z|A-Z]*[0-9]+[a-z|A-Z|0-9]*");

    @Override
    public void addUser(UserVO userVO) {
        boolean isParamValid = StringUtils.isNotBlank(userVO.getId()) &&
                StringUtils.isNotBlank(userVO.getName()) &&
                StringUtils.isNotBlank(userVO.getPassword());

        isParamValid = isParamValid &&
                StringUtils.length(userVO.getPassword()) >= 6
                && userVO.getPassword().matches(pattern.pattern());


        if(!isParamValid){
            throw new RuntimeException("[회원가입 에러] 정합성 오류");
        }
    }
}
package com.example.test5.Service;

import com.example.test5.Model.Entity.UserEntity;
import com.example.test5.Model.VO.UserVO;
import com.example.test5.Service.Impl.UserServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class UserServiceTest {

    @InjectMocks
    private final UserService userService = new UserServiceImpl();

    private UserVO testUser;

 
    @Test
    @DisplayName("아이디가 Null일떄")
    public void testAddUserWhenIdNull() {
        //fixture값 재수정
        testUser.setId(null);

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });
    }

    @Test
    @DisplayName("비밀번호가 이상한거일떄")
    public void testAddUserWhenNotInvalidPassword(){
        testUser.setPassword("sadklasdjaskl");

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });
    }


}
  • ID 중복체크
 @Override
    public void addUser(UserVO userVO) {
        boolean isParamValid = StringUtils.isNotBlank(userVO.getId()) &&
                StringUtils.isNotBlank(userVO.getName()) &&
                StringUtils.isNotBlank(userVO.getPassword());

        isParamValid = isParamValid &&
                StringUtils.length(userVO.getPassword()) >= 6
                && userVO.getPassword().matches(pattern.pattern());


        if(!isParamValid){
            throw new RuntimeException("[회원가입 에러] 정합성 오류");
        }

        if(userRepository.existUserById(userVO.getId())){
            throw new RuntimeException("[회원가입 에러] 중복된 유저 아이디");
        }

        UserEntity userEntity = UserEntity.builder()
                .id(userVO.getId())
                .name(userVO.getName())
                .password(userVO.getPassword())
                .build();

        userRepository.save(userEntity);
    }
    @Test
    public void testAddUser() {
        when(userRepository.existUserById(testUser.getId())).thenReturn(false);

        userService.addUser(testUser);

        verify(userRepository, times(1)).existUserById(testUser.getId());
    }

    @Test
    @DisplayName("DB에 중복된 ID가 존재할 떄")
    public void testAddUserWhenDuplicatedId(){
        when(userRepository.existUserById(testUser.getId())).thenReturn(true);

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });

        verify(userRepository, times(1)).existUserById(testUser.getId());
    }
  • 회원가입 완료
 @Override
    public void addUser(UserVO userVO) {
        boolean isParamValid = StringUtils.isNotBlank(userVO.getId()) &&
                StringUtils.isNotBlank(userVO.getName()) &&
                StringUtils.isNotBlank(userVO.getPassword());

        isParamValid = isParamValid &&
                StringUtils.length(userVO.getPassword()) >= 6
                && userVO.getPassword().matches(pattern.pattern());


        if(!isParamValid){
            throw new RuntimeException("[회원가입 에러] 정합성 오류");
        }

        if(userRepository.existUserById(userVO.getId())){
            throw new RuntimeException("[회원가입 에러] 중복된 유저 아이디");
        }

        UserEntity userEntity = UserEntity.builder()
                .id(userVO.getId())
                .name(userVO.getName())
                .password(userVO.getPassword())
                .build();

        userRepository.save(userEntity);
    }
   @Test
    public void testAddUser() {
        when(userRepository.existUserById(testUser.getId())).thenReturn(false);

        userService.addUser(testUser);

        verify(userRepository, times(1)).existUserById(testUser.getId());
        verify(userRepository, times(1)).save(any(UserEntity.class));
    }
  • 테스트 수정하기 (좀더 완벽하게)

    에러가 뜨면 save가 실행이 되나 안되나, 중간 중간 로직들이 실행이 되나 안되나, 코드가 어디서 멈추는지 테스트 코드를 구체화하자.

@SpringBootTest
public class UserServiceTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UserRepository userRepository;

    private UserVO testUser;

    @BeforeEach
    public void setUp() {

        testUser = UserVO.builder()
                .id("test1")
                .password("test1234")
                .name("테스트 유저").build();
    }

    @Test
    public void testAddUser() {
        when(userRepository.existUserById(testUser.getId())).thenReturn(false);

        userService.addUser(testUser);

        verify(userRepository, times(1)).existUserById(testUser.getId());
        verify(userRepository, times(1)).save(any(UserEntity.class));
    }

    @Test
    @DisplayName("아이디가 Null일떄")
    public void testAddUserWhenIdNull() {
        //fixture값 재수정
        testUser.setId(null);

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });

        verify(userRepository, never()).existUserById(testUser.getId());
    }

    @Test
    @DisplayName("비밀번호가 이상한거일떄")
    public void testAddUserWhenNotInvalidPassword(){
        testUser.setPassword("sadklasdjaskl");

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });

        verify(userRepository, never()).existUserById(testUser.getId());
    }

    @Test
    @DisplayName("DB에 중복된 ID가 존재할 떄")
    public void testAddUserWhenDuplicatedId(){
        when(userRepository.existUserById(testUser.getId())).thenReturn(true);

        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.addUser(testUser);
        });

        verify(userRepository, times(1)).existUserById(testUser.getId());
        verify(userRepository, never()).save(any(UserEntity.class));
    }


}
  • 이제 코드의 로직이 바뀌어도 테스트 코드에서 기본 설계가 짜여져 있기 떄문에, 리팩토링이 일어나도 테스트 코드만 무사히 진행된다면 결과를 어느정도 안심할 수 있다.
  • 책임분리가 잘된 상태에서, 유닛 테스트를 잘해놓으면 유지보수에 진짜 편한 코드가 되니 참고

결론

  1. 테스트 코드를 잘짜자
  2. 최소한 유닛 테스트는 하자
  3. 위에서 보여줬던 것처럼 TDD를 할 때는 다음 2계명을 명심하자
    1. 실패하는 테스트를 고려해서 짤 것
    2. 설계 - 테스트 - 개발의 주기가 짧으면 좋을 것
  4. 통합테스트도 해보면 좋다.
  5. 테스트하는 방법과 기법도 여러가지 많으니 빡세게 돌리자

쓰고나니 코드는 많고 큰 내용은 없다. 그치만 테스트 중요하니 화이링

암튼 끝

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글