개발자가 말대꾸?
봄 백엔드 개발일기
개발자가 말대꾸?
전체 방문자
오늘
어제
  • 분류 전체보기 (42)
    • 알고리즘 공부 (13)
    • 디자인 패턴 공부 (1)
    • Spring (15)
      • Spring Boot (12)
      • Spring Data (1)
      • Spring Security (1)
    • Java (2)
    • MySQL (5)
    • EDITOR (3)
      • Intellij (3)
      • vscode (0)
    • 기타 (3)
      • 에러 (3)
      • 감상문 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • intelliJ 단축키
  • IntelliJ
  • MSA 아키텍처에서 Config Server의 변경 사항을 MSA에게 전달하는 방법
  • rest-api
  • 인텔리제이 좋은점
  • Jpa 다중 제약조건 설정
  • Python
  • JPA 여러 컬럼 유니크
  • JPA Unique 제약조건
  • 인텔리제이 사용법
  • spring boot
  • BasicAuthenticationFilter
  • JPA 여러 컬럼 Unique
  • 프로그래머스
  • 프로그래머스 2단계
  • 라이브 템플릿
  • 코드 템플릿
  • Java
  • RabbitMQ Kafka 차이
  • BasicAuthorization
  • spring
  • intellij live templates
  • jsp
  • 권한 프로그래밍
  • GrantedAuthority
  • SpringBoot
  • JPA
  • UserDetails 도메인
  • mysql
  • SpringSecurity 프로젝트

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
개발자가 말대꾸?

봄 백엔드 개발일기

[학교 관리 프로젝트] 도메인 단위 테스트와 테스트 코드를 가독성있고 빠르게 작성하는 방법
Spring/Spring Boot

[학교 관리 프로젝트] 도메인 단위 테스트와 테스트 코드를 가독성있고 빠르게 작성하는 방법

2022. 8. 31. 04:57

 

테스트할 도메인은 아래 포스트를 참고하자.

2022.08.31 - [학교 관리 서비스 프로젝트] - [학교 관리 프로젝트] UserDetails를 구현한 User 도메인, @Query를 사용한 JPQL과 서비스 코드 구현

 

[학교 관리 프로젝트] UserDetails를 구현한 User 도메인, @Query를 사용한 JPQL과 서비스 코드 구현

UserDetails.java UserDetails를 상속한 클래스는 Abstract하거나 아래의 메서드를 완성시켜야한다 반환타입이 boolean 값인 메서드는 이 계정이 유효한지 판단하는 메서드다. 접속한지 90일이 지났거나, 신

tae-wk.tistory.com

 

 

한글 메서드 명 사용하기

실무에서 사용할지 모르겠지만, 나의 경우 테스트 코드의 메서드 명은 주로 한글로 작성하는 편이다. 

 

Comment가 없어도 알아볼 수 있는 코드가 가장 좋은 코드라고 배웠지만 그럴 자신은 없고 Comment가 귀찮을 때 사용하길 추천한다. 

 

TeacherTest.java

package com.schoolproject.user.service;

/**
 * @author Taewoo
 */

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class TeacherTest extends WithUserTest {

    User teacher;

    @BeforeEach
    void before() {
        prepareUserServices();
        this.teacher = this.userTestHelper.createTeacher(school, "teacher1");
    }

    @DisplayName("1. 선생님 등록")
    @Test
    void 선생님_등록() {
        var teacherList = userService.findTeacherList();
        assertEquals(1, teacherList.size());
        UserTestHelper.assertTeacher(school, teacherList.get(0), "teacher1");
    }

    @DisplayName("2. 선생님으로 등록한 학생 리스트 조회")
    @Test
    void 선생님으로_등록한_학생_리스트_조회() {
        this.userTestHelper.createStudent(school, teacher, "study1", "1");
        this.userTestHelper.createStudent(school, teacher, "study2", "1");
        this.userTestHelper.createStudent(school, teacher, "study3", "1");
        assertEquals(3, userService.findTeacherStudentList(teacher.getUserId()).size());
    }


    @DisplayName("3. 선생님 리스트 조회")
    @Test
    void 선생님_리스트() {
        this.userTestHelper.createUser(school, "teacher2", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "teacher3", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "teacher4", Authority.ROLE_TEACHER);

        assertEquals(4, userService.findTeacherList().size());
    }

    @DisplayName("4. 선생님 조회 테스트")
    @Test
    void 선생님_조회() {
        var list = new ArrayList<>(
                List.of(this.userTestHelper.createTeacher(school, "신민화"),
                        this.userTestHelper.createTeacher(school, "허순희"),
                        this.userTestHelper.createTeacher(school, "장용미"),
                        this.userTestHelper.createTeacher(school, "박성우")));

        assertTrue(userService.findTeacherList().containsAll(list));
    }

    @DisplayName("5. 학교로 선생님 조회")
    @Test
    void 학교로_선생님_조회() {
        var teacherList = userService.findBySchoolTeacherList(school.getSchoolId());
        assertEquals(1, teacherList.size());
        UserTestHelper.assertTeacher(school, teacher, "teacher1");

        this.userTestHelper.createUser(school, "허순희", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "장용미", Authority.ROLE_TEACHER);

        assertEquals(3, userService.findTeacherList().size());
    }
}

 

 

 

@ComponentScan 사용해서 모듈 불러오기

 

PaperUserTestApp.java

@SpringBootApplication
public class PaperUserTestApp {

    public static void main(String[] args) {
        SpringApplication.run(PaperUserTestApp.class, args);
    }

    @Configuration
    @ComponentScan("com.schoolproject.user")
    @EnableJpaRepositories(basePackages = {
            "com.schoolproject.user.repository"
    })
    @EntityScan(basePackages = {
            "com.schoolproject.user.domain"
    })
    class Config {
        
    }
}

 

나의 경우 로직코드에 아무 오류가 없었고 Swagger나 POSTMAN을 이용한 테스트는 잘되지만 JUnit을 사용한 테스트가 에러가 났다.

 

DI가 안되거나 Bean 등록이 안되기도 하고 무엇보다 가독성이 별로 안좋았다. 

 

@SpringBootTest를 @SpringBootApplication 어노테이션으로 교체해준다.

 

우리가 만든 로직을 테스트코드에서 잘 인식할 수 있도록  Inner 클래스를 만들어 @Configuration 어노테이션을 붙인다.  

 

@ComponentScan으로 인식할 패키지의 경로를 잘 입력한다. Intellij의 경우 디렉토리 트래킹을 지원하기 때문에 쉽게 작성할 수 있다.

 

@EnableJpaRepositories의 basePackage 속성에 Jpa레포지토리를 상속받은 인터페이스의 패키지 경로를 입력해준다.

 

@EntityScan의 basePackage 속성에 도메인 객체가 있는 패키지 경로를 입력해준다.

 

Helper, Util 클래스 만들기

어떤 테스트를 하려면 데이터를 준비해야된다.

 

로직 상 School은 많은 Student를 가진다고 가정한다.

 

Student 'A'를 생성해서 School에 넣고 School에서 Student들을 꺼내 'A'를 찾는다면 

 

테스트 하나에 School배열을 가져오는 코드, Student를 순회하며 비교하는 코드, Student A를 생성하는 코드, assert할 코드까지 로직이 복잡해진다.

 

그래서 Util Class, Helper Class를 만든다.

 

WithUserTest

public class WithUserTest {

    @Autowired
    protected SchoolRepository schoolRepository;

    @Autowired
    protected UserRepository userRepository;

    protected SchoolService schoolService;
    protected UserService userService;
    protected UserSecurityService userSecurityService;

    protected SchoolTestHelper schoolTestHelper;
    protected UserTestHelper userTestHelper;
    protected School school;

    private boolean prepared;

    protected void prepareUserServices() {
        if (prepared) return;
        prepared = true;

        this.schoolRepository.deleteAll();
        this.userRepository.deleteAll();
        this.schoolService = new SchoolService(schoolRepository);
        this.userService = new UserService(schoolRepository, userRepository);
        this.userSecurityService = new UserSecurityService(userRepository);

        this.userTestHelper = new UserTestHelper(userService, NoOpPasswordEncoder.getInstance());
        this.schoolTestHelper = new SchoolTestHelper(schoolService);
        this.school = this.schoolTestHelper.createSchool("테스트 학교", "서울");
    }
}

 

User 객체가 필요한 테스트클래스에 위 클래스를 상속받고 시작한다.

 

모든 단위 테스트는 "순서"와 "환경"에 상관없이 원하는 로직을 성공시켜야한다.

 

위 클래스를 상속받으면 필요한 의존성들을 모두 주입받고 들어가기 때문에 테스트에서 의존성 주입 실패로 인한 NPE걱정을 안해도된다.

 

또 Repository 클래스를 모두 비워주는 작업을 하기 때문에 매번 지울 필요가 없다. (유닛테스트마다 Case by case지만 통합 테스트의 경우 대부분 테스트가 끝나면 모두 롤백하는 작업을 하는 것으로 알고 있다.)

 

내 코드의 경우 테스트에 필요한 도메인 객체도 미리 만들고 시작하도록 했다.

 

아래는 다른 Helper 클래스다.

 

SchoolTestHelper.java

@RequiredArgsConstructor
public class SchoolTestHelper {

    private final SchoolService schoolService;

    public static School makeSchool(String name, String city) {
        return School.builder().name(name).city(city).build();
    }

    public School createSchool(String name, String city) {
        return schoolService.save(makeSchool(name, city));
    }

    public static void assertSchool(School school, String name, String city) {
        assertNotNull(school.getSchoolId());
        assertNotNull(school.getCreated());
        assertNotNull(school.getUpdated());

        assertEquals(name, school.getName());
        assertEquals(city, school.getCity());
    }
}

 

UserTestHelper.java

@RequiredArgsConstructor
public class UserTestHelper {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;

    public static User makeUser(School school, String name) {
        return User.builder()
                .school(school)
                .name(name)
                .email(name + "@test.com")
                .enabled(true)
                .build();
    }

    public User createUser(School school, String name) {
        User user = makeUser(school, name);
        user.setPassword(passwordEncoder.encode(name + "1234"));
        return userService.save(user);
    }

    public User createUser(School school, String name, String... authorities) {
        User user = createUser(school, name);
        Stream.of(authorities).forEach(auth -> userService.addAuthority(user.getUserId(), auth));
        return user;
    }

    public User createTeacher(School school, String name) {
        User teacher = createUser(school, name);
        userService.addAuthority(teacher.getUserId(), Authority.ROLE_TEACHER);
        return teacher;
    }

    public static void assertTeacher(School school, User teacher, String name) {
        assertUser(school, teacher, name, Authority.ROLE_TEACHER);
    }

    public User createStudent(School school, User teacher, String name, String grade) {
        User student = User.builder()
                .school(school)
                .name(name)
                .password(passwordEncoder.encode(name + "123"))
                .email(name + "@test.com")
                .teacher(teacher)
                .grade(grade)
                .enabled(true)
                .build();

        student = userService.save(student);
        userService.addAuthority(student.getUserId(), Authority.ROLE_STUDENT);
        return student;
    }

    public static void assertStudent(School school, User teacher, User student, String name, String grade) {
        assertUser(school, student, name, Authority.ROLE_STUDENT);
        assertEquals(teacher.getUserId(), student.getTeacher().getUserId());
        assertEquals(grade, student.getGrade());
    }

    public static void assertUser(School school, User user, String name) {
        assertNotNull(user.getUserId());
        assertNotNull(user.getCreated());
        assertNotNull(user.getUpdated());
        assertTrue(user.isEnabled());
        assertEquals(school.getSchoolId(), user.getSchool().getSchoolId());
        assertEquals(name, user.getName());
        assertEquals(name + "@test.com", user.getEmail());
    }

    public static void assertUser(School school, User user, String name, String... authorities) {
        assertUser(school, user, name);

        assertTrue(user.getAuthorities().containsAll(
                Stream.of(authorities).map(auth -> new Authority(user.getUserId(), auth))
                        .collect(Collectors.toSet())
        ));
    }
}

 

필요에 따라 Helper 클래스에서도 assertTrue, assertEquals처럼 테스트하도록 만들었다.

 

3. CodeTemplates를 사용한 테스트 코드 작성

빠른 테스트를 위해 만든 템플릿이다. 인텔리제이를 사용한다면 적용해도 괜찮을 것같다.

 

@DisplayName("$DisplayName$")
@Test
void $MethodName$(){
 $END$
}

사용하는 방법은 'test'를 입력하고 Tab을 누르면 자동으로 위 코드가 작성된다.

 

코드 템플릿 사용방법은 아래 포스트를 참고해주면 좋겠다.

 

2022.08.28 - [Spring/Spring Security] - [Spring Security] BasicAuthenticationFilter와 TestRestTemplates을 사용한 테스트코드 작성

 

[Spring Security] BasicAuthenticationFilter와 TestRestTemplates을 사용한 테스트코드 작성

적용하는 방법 @EnableWebSecurity(debug = true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configur..

tae-wk.tistory.com

 

'Spring > Spring Boot' 카테고리의 다른 글

[Spring Boot] 요청, 응답 시 Json에 루트 추가하기  (0) 2022.09.24
객체지향의 사실과 오해를 읽으며 코드 리팩토링  (0) 2022.09.19
[게시판 RESTful API] - RestController와 Service를 구현해보자 (3)  (0) 2022.08.20
[게시판 RESTful API] - domain을 만들며 Entity를 알아보자. (2)  (0) 2022.08.20
[게시판 RESTful API] - 프로젝트 세팅 (1)  (0) 2022.08.20
    'Spring/Spring Boot' 카테고리의 다른 글
    • [Spring Boot] 요청, 응답 시 Json에 루트 추가하기
    • 객체지향의 사실과 오해를 읽으며 코드 리팩토링
    • [게시판 RESTful API] - RestController와 Service를 구현해보자 (3)
    • [게시판 RESTful API] - domain을 만들며 Entity를 알아보자. (2)
    개발자가 말대꾸?
    개발자가 말대꾸?
    - ing9990.com - 열정적인 ENTP - 주말 코딩, 퇴근 코딩 ing9990

    티스토리툴바