MVC패턴에서 테스트 코드는 크게 3가지로 분류됩니다. Controller 테스트 코드, Service 테스트 코드 그리고 Repository 테스트 코드입니다.

여기서 Controller 테스트와 Service 테스트는 실제 데이터를 받아서 처리하지 않고 Mock으로 처리하여 진행하게 됩니다.

실제로 계층을 나눈 이유는 이러한 독립성을 유지하기 위해서 입니다.

이 글에서는 MongoDB와 Repository코드에서 어떻게 테스트 코드를 작성하는 방법을 소개합니다.


코드

@DataMongoTest
class DomainRepositoryTest {
    private DomainRepository repository;

    @Autowired
    public DomainRepositoryTest(DomainRepository repository) {
        this.repository = repository;
    }

    @Test
    void Domain_정상_저장() {
        Domain domain = new Domain("테스트 데이터");

        repository.save(domain);
        Domain domain = repository.findByEmail("테스트 데이터");

        assertNotNull(domain);
        assertEquals("테스트 데이터", domain.getEmail());
        repository.deleteDomain(domain);
    }

코드 해석

@DataMongoTest

Mongo Test 어노테이션으로 전 글에서 작성했던 Embedded Mongo Test를 들고 와서 설정합니다.

생성자

@Autowired
public DomainRepositoryTest(DomainRepository repository) {
    this.repository = repository;
}
여기서 생성자는 Bean으로 등록된 Repository를 테스트 코드에 주입해줍니다. 이때, Repository는 Embedded DB와 연결되어 있습니다.
더보기

이때, DomainRepository는 MongoRepository<Domain, IDType>을 확장받은 Interface이여야 한다.

public interface DomainRepository extends MongoRepository<Domain, BigInteger> {
    Domain findByEmail(String name);
}

테스트 코드

@Test void Domain_정상_저장() {
    Domain domain = new Domain("테스트 데이터");
    repository.save(domain);

    Domain domain = repository.findByEmail("테스트 데이터");

    assertNotNull(domain);
    assertEquals("테스트 데이터", domain.getEmail());
    repository.deleteDomain(domain);
}
Domain domain = new Domain("테스트 데이터");
repository.save(domain);
처음에 2줄은 데이터를 생성합니다.

Domain domain = repository.findByEmail("테스트 데이터");
저장된 데이터를 다시 들고 옵니다. (이때, 저장한 공간은 Embedded DB이므로, 테스트 객체가 살아있는 동안에만 메모리로 저장하고, 테스트 객체가 자원을 반납하면 데이터도 삭제됩니다.
따라서, 여러개의 테스트를 같은 객체 내에서 동시에 작동시킨다면 데이터의 충돌을 생각하여 테스트 데이터를 작성해야 합니다.

assertNotNull(domain);
assertEquals("테스트 데이터", domain.getEmail());
repository.deleteDomain(domain);
Embedded 데이터에서 나온 객체와 DB에 저장하기 전 데이터가 같은지 확인합니다. 이때, assertThat같은 함수는 junit의 테스트 프레임워크를 사용하였습니다.

마무리

이렇게 Repository의 테스트 코드를 작성하는 방법에 대해서 살펴보았습니다.

항상 테스트 데이터는 실제 DB에 반영되서는 안되며, 테스트를 위한 DB를 따로 설정하거나 이번에 포스팅한 내용처럼 MemoryDB를 사용해야 합니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

NoSQL 데이터베이스 중 가장 많이 활용되는 MongoDB는 최근 AI와 빅데이터의 성장과 함께 더욱 주목받고 있습니다. 저의 프로젝트 역시 AI를 활용한 서비스를 제공하기 때문에 MongoDB를 도입하게 되었습니다. 이번 글에서는 Spring 환경에서 MongoDB를 설정하는 방법을 소개합니다.

MongoDB 설치하기

Gradle를 통해서 간단하게 라이브러리를 설치할 수 있습니다.

implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

MongoDB 설정하기

1. 간단한 설정

기본적으로 application.yml 파일에 MongoDB 연결 정보를 다음과 같이 작성하면 설정이 완료됩니다:

spring:
    data:
    	mongodb:
            uri=mongodb://localhost:27017/mydb

이 방식은 간단한 연결이 필요한 경우 유용합니다. 하지만 프로젝트에서 더 복잡한 설정이 필요하다면 MongoDBConfig 클래스를 생성하여 직접 설정할 수 있습니다.

2. 커스텀 설정

추가적인 설정이 필요한 경우 다음과 같이 설정 클래스를 작성합니다:

 

@Configuration
@EnableMongoRepositories(basePackages = "repository가 저장된 위치")
public class MongoDBConfig extends AbstractMongoClientConfiguration {
    @Value("${spring.data.mongodb.url}")
    private String connectionUrl;

    @NotNull
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString(connectionUrl);
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
                .uuidRepresentation(UuidRepresentation.STANDARD)
                .applyConnectionString(connectionString)
                .build();
        return MongoClients.create(mongoClientSettings);
    }

    @NotNull
    @Override
    protected String getDatabaseName() {
        return "mydb";
    }
}

이 방식은 프로젝트의 요구사항에 맞게 세부적인 설정을 적용할 수 있습니다. 특히, uuidRepresentation과 같은 속성은 데이터베이스 보안을 강화하는 데 유용합니다.

 

마무리

이번 글에서는 Spring에서 MongoDB를 설정하는 두 가지 방법을 살펴보았습니다. 간단한 프로젝트는 application.yml 파일을 통해 빠르게 설정할 수 있고, 복잡한 설정이 필요한 경우 MongoDBConfig 클래스를 활용하면 유연하게 대응할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

Service Test의 맹점

이전에 Repository 테스트 코드를 Embedded DB를 활용하여 작성하는 방법에 대해 포스팅했었습니다. 오늘은 Repository를 의존하고 있는 Service를 어떻게 테스트할 수 있는지 알아보겠습니다.

@DataMongoTest의 단점

@DataMongoTest는 @SpringBootTest와 함께 사용할 수 없을 뿐만 아니라, 테스트 코드 작성 시 의존성 주입이 까다롭다는 단점이 있습니다. @DataMongoTest를 사용하지 않고 진행하려 해도, MongoRepository<Domain, ID>를 상속받은 Repository 구현체가 필요하기 때문에 의존성 문제가 발생하게 됩니다.

이 문제를 해결하기 위한 방법은 무엇일까요?

해결책: Mock 사용하기

Mock은 빈 구현체를 만들어서 주입하는 방식으로, 실제 Repository를 대체하여 테스트할 수 있게 해줍니다. 이를 자동으로 처리해주는 프레임워크로는 Mockito가 있는데, 이 프레임워크의 사용법에 대해서는 이후 포스팅에서 다루도록 하겠습니다.

Service 코드

다음은 프로젝트의 Service 클래스 코드입니다. 이 클래스는 Repository를 의존하고 있으며, 아래의 joinNewUserFromDefault 메서드를 통해 새 사용자를 저장하는 역할을 합니다.

@Slf4j @Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
    private final ValidateUserData repository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional
    public API<Object> joinNewUserFromDefault(UserJoinDto dto) {
        String encodePassword = bCryptPasswordEncoder.encode(dto.getPassword());
        UserData newOffspring = UserData.builder()
                .email(dto.getEmail())
                .userName(dto.getUserName())
                .password(encodePassword)
                .provider(SsoType.DEFAULT)
                .userType(UserType.OFFSPRING).build();
            repository.saveNewUserData(newOffspring);
        }
        return new API<>(APISuccessMessage.회원가입_성공);
    }
}

Service Test 코드 (Mock 없이 작성한 경우)

다음은 @DataMongoTest를 사용하여 작성한 테스트 코드입니다. 이 경우, @Bean에 대한 검증이나 관련 테스트를 작성하기 어려운 단점이 있습니다.

@DataMongoTest
class UserServiceTest {
    private UserService userService;

    UserServiceTest(@Autowired UserDataRepository repository) {
        ValidateUserData repo = new ValidateUserData(repository);
        this.userService = new UserService(repo, new BCryptPasswordEncoder());
    }

    @Test
    void 신규가입_확인() {
        UserJoinDto dto = new UserJoinDto("admin@gmail.com", "admin", "1234qwer", "OFFSPRING", "DEFAULT");
        API<Object> result = userService.joinNewUserFromDefault(dto);
        assertThat(result.getApiMessage().getMessage()).isEqualTo(APISuccessMessage.회원가입_성공.getMessage());
    }
}

Service Test 코드 (Mock으로 작성한 경우)

위의 문제를 해결하기 위해 Mock을 사용하여 테스트 코드를 작성할 수 있습니다. 아래는 Mock을 이용한 테스트 코드입니다.

class UserServiceTest {
    private UserService userService;

    UserServiceTest() {
        ValidateUserData repository = new ValidateUserData(new MockUserDataRepository());
        this.userService = new UserService(repository, new BCryptPasswordEncoder());
    }

    @Test
    void 신규가입_확인() {
        UserJoinDto dto = new UserJoinDto("admin@gmail.com", "admin", "1234qwer", "OFFSPRING", "DEFAULT");
        API<Object> result = userService.joinNewUserFromDefault(dto);
        assertThat(result.getApiMessage().getMessage()).isEqualTo(APISuccessMessage.회원가입_성공.getMessage());
    }
}

MockUserDataRepository 구현체

아래는 MockUserDataRepository 구현체입니다. 이 클래스는 MongoDBRepository를 상속받아 구현하며, 메서드들을 모두 Override하면 된다.

 

더보기
더보기

MockUserDataRepository 구현체이다. MongoDBRepository를 상속받아서 구현하였으므로, 매우 길고 복잡하지만, 그냥 Override를 받아서 처리하면 된다. 필요한 리턴 타입은 따로 정의하면 될 것 같다.

public class MockUserDataRepository implements UserDataRepository {
    public MockUserDataRepository() {
        super();
    }

    @Override
    public Optional<UserData> findByEmail(String email) {
        return Optional.empty();
    }

    @Override
    public void deleteByEmail(String email) {

    }

    @Override
    public <S extends UserData> S insert(S entity) {
        return null;
    }

    @Override
    public <S extends UserData> List<S> insert(Iterable<S> entities) {
        return List.of();
    }

    @Override
    public <S extends UserData> List<S> findAll(Example<S> example) {
        return List.of();
    }

    @Override
    public <S extends UserData> List<S> findAll(Example<S> example, Sort sort) {
        return List.of();
    }

    @Override
    public <S extends UserData> List<S> saveAll(Iterable<S> entities) {
        return List.of();
    }

    @Override
    public List<UserData> findAll() {
        return List.of();
    }

    @Override
    public List<UserData> findAllById(Iterable<String> strings) {
        return List.of();
    }

    @Override
    public <S extends UserData> S save(S entity) {
        return null;
    }

    @Override
    public Optional<UserData> findById(String s) {
        return Optional.empty();
    }

    @Override
    public boolean existsById(String s) {
        return false;
    }

    @Override
    public long count() {
        return 0;
    }

    @Override
    public void deleteById(String s) {

    }

    @Override
    public void delete(UserData entity) {

    }

    @Override
    public void deleteAllById(Iterable<? extends String> strings) {

    }

    @Override
    public void deleteAll(Iterable<? extends UserData> entities) {

    }

    @Override
    public void deleteAll() {

    }

    @Override
    public List<UserData> findAll(Sort sort) {
        return List.of();
    }

    @Override
    public Page<UserData> findAll(Pageable pageable) {
        return null;
    }

    @Override
    public <S extends UserData> Optional<S> findOne(Example<S> example) {
        return Optional.empty();
    }

    @Override
    public <S extends UserData> Page<S> findAll(Example<S> example, Pageable pageable) {
        return null;
    }

    @Override
    public <S extends UserData> long count(Example<S> example) {
        return 0;
    }

    @Override
    public <S extends UserData> boolean exists(Example<S> example) {
        return false;
    }

    @Override
    public <S extends UserData, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
        return null;
    }
}

Mock을 사용하면 추가적인 프레임워크나 설정 없이 테스트를 진행할 수 있습니다. 작은 프로젝트의 경우 이런 방식으로 테스트를 작성하는 것이 유용할 수 있습니다.

 

혹시라도 수정할 부분이 있다면 댓글로 알려주세요!

Embedded DB Test

Embedded DB란 메모리 데이터 베이스를 의미한다. 이렇게 생각하면 왜 이미 존재하는 RDBMS나 Json형식의 MongoDB에서 메모리 데이터 베이스를 제공하는 것일까??

 

일단 Test에 사용하는 DB는 서비스와 구분되어야 한다. 서비스되고 있는 DB와 같이 사용하게 된다면 실제 데이터와 테스트를 위한 데이터가 혼합되어, 실제 데이터가 변질될 가능성이 존재한다. 따라서 테스트를 위한 DB는 크게 2가지가 존재한다.

 

1. 테스트용 DB 제작

2. Memory DB 제작

 

여기서 나는 2번을 사용하기로 결정하였다. 이번 프로젝트는 Docker를 사용해서 빌드를 하는데, 이때 2개의 DB를 만들어서 연결하는 것이 좀 번거롭고, 서버 컴퓨터의 성능을 제한한다고 생각했기 때문이다. (여기서 Memory DB는 Embedded DB라고 말하고 있다.)

 

근데 많은 블로그에서 사용하고 있는 Embedded MongoDB 설정을 했음에도, 실제 DB와 연결되어 실행되는 오류가 계속 발견하였다.

더보기

Error creating bean with name 'syncClientServerWrapper' defined in class path resource [de/flapdoodle/embed/mongo/spring/autoconfigure/EmbeddedMongoAutoConfiguration$SyncClientServerWrapperConfig.class]: Unsatisfied dependency expressed through method 'syncClientServerWrapper' parameter 0: Error creating bean with name 'version' defined in class path resource [de/flapdoodle/embed/mongo/spring/autoconfigure/EmbeddedMongoAutoConfiguration.class]: Failed to instantiate [de.flapdoodle.embed.mongo.distribution.IFeatureAwareVersion]: Factory method 'version' threw exception with message: Set the de.flapdoodle.mongodb.embedded.version property or define your own IFeatureAwareVersion bean to use embedded MongoDB

음... 내 설정이 이상한건가 하고 계속 찾다보니 마땅히 해결법은 안 나오고 MongoTest를 만든 git까지 찾아서 들어가게 되었다.

https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues/457

 

Integration Testing is failing · Issue #457 · flapdoodle-oss/de.flapdoodle.embed.mongo

I am trying to have some a Service Integration test and Controller Integration test: Java Version 17 Spring Boot version: 3.0.5 de.flapdoodle.embed : de.flapdoodle.embed.mongo.spring30x: version is...

github.com

여기서 작성자는 신기하게도 Spring boot가 실행되면서 MongoDB port가 27017로 되는 것 같다고 추측하면서 test.resources.application.yml 파일에 이렇게 작성하라고 하고 있습니다.

spring:
  application:
    name: backend
    embedded: true # 임베디드 DB 사용 명시
  data:
    mongodb:
      port: 0
de:
  flapdoodle:
    mongodb:
      embedded:
        version: 5.0.0

저도 이 방식으로 Embedded Mongo DB를 사용할 수 있게 되었습니다. 저처럼 Spring + MongoDB 에서 Embedded DB를 사용해서 Test를 작성할려고 시도하나 에러가 나오는 분들은 이렇게 한번 시도해보는 것을 추천합니다~

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

+ Recent posts