testcontainer 사용시 DB 클렌징

greenTea·2024년 5월 22일
0

testcontainer db 클렌징

testcontainer를 이용하여 테스트 코드를 작성하과 있는 도중에 데이터 클렌징과 관련하여 고민이 생겼고 여러 자료들을 통해 junint의 extension을 이용하여 관리할 수 있다는 것을 알게 되었고 이를 정리해보았습니다.

testcontainer 설정

public abstract class IntegrationTestContainers {

	static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo")
		.withReuse(true);

	static final GenericContainer<?> redisContainer = new GenericContainer<>(DockerImageName.parse("redis"))
		.withExposedPorts(6379);

	static {
		mongoDBContainer.start();
		redisContainer.start();
	}

	@DynamicPropertySource
	static void setProperties(DynamicPropertyRegistry registry) {
		setMongoProperties(registry);
		setRedisProperties(registry);
	}

	private static void setMongoProperties(DynamicPropertyRegistry registry) {
		registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
	}

	private static void setRedisProperties(DynamicPropertyRegistry registry) {
		registry.add("spring.data.redis.host", () -> redis.getHost());
		registry.add("spring.data.redis.port", () -> redis.getFirstMappedPort());
		registry.add("spring.data.redis.password", () -> "1111");
		registry.add("spring.data.redis.database", () -> 0);
	}

}

저는 싱글톤 방식으로 띄위기 위해 위와 같이 구현하였습니다.
@TestContainer, @Containers 어노테이션을 사용할 경우 자동으로 container를 띄우고 종료시켜줍니다.

그러나 사용해보면 여러 테스트 실행시 docker image를 띄우고 종료를 하는 시간과 엇갈리게 되면서 테스트에서 해당 image를 이용하지 못하고 에러가 나기 때문에 위처럼 어노테이션을 사용하지 않고 static으로 직접 실행 시켜주었습니다.

또한 @DynamicPropertySource를 이용하여 동적으로 프로퍼티를 집어넣었습니다. testcontainer의 경우 랜덤으로 포트를 열기 때문에 기존에 사용하던 방식을 사용하면 찾지 못하게 되므로 getFirstMappedPort()를 사용하여 직접 주입해주었습니다.

DB 관리

위 방식을 이용하면 아래와 같은 방식으로 사용 할 수 있습니다.

class CustomTest extends IntegrationTestContainers {

	@BeforeEach
    void delete() {
    	repository.delete()
    }
}

위와 같이 @BeforeEach를 이용하여 직접 테스트 종료시 데이터를 삭제해도 되지만 junit에서 지원하는 BeforeEachCallback를 사용하면 자동으로 테스트 시작전 데이터를 지울 수 있습니다.
junit docs

먼저 데이터를 지우는 클래스를 만들겠습니다.

Db 데이터 삭제 class

@Slf4j
@TestComponent
public class ClearDatabase {

	@Autowired
	private RedisTemplate redisTemplate;
	@Autowired
	private MongoTemplate mongoTemplate;

	public void clear() {

		redisTemplate.execute((RedisConnection connection) -> {
			connection.flushDb();
			log.debug("drop redis");
			return null;
		});

		Set<String> collectionNames = mongoTemplate.getCollectionNames();
		for (String collectionName : collectionNames) {
			mongoTemplate.dropCollection(collectionName);
		}
		
	}
}

여기서는 redis와 mongodb의 데이터를 삭제하는 방법으로 구현하였습니다.
(데이터 삭제의 경우 위와 같은 방식 외에도 다양한 방법이 있기에 이용하시면 될 것 같습니다. 저의 경우 로그를 확인하기 위해 위와 같이 구현하였습니다.)

다음은 junit이 지원하는 extension을 이용하여 라이프 사이클을 조절해보겠습니다.

Extension

public class ClearExtension implements BeforeEachCallback {
	@Override
	public void beforeEach(ExtensionContext context) throws Exception {
		ClearDatabase databaseCleaner = getDataCleaner(context);
		databaseCleaner.clear();
	}

	private ClearDatabase getDataCleaner(ExtensionContext extensionContext) {
		return SpringExtension.getApplicationContext(extensionContext)
							  .getBean(ClearDatabase.class);
	}
}

이제 extension을 사용할 수 있도록 등록하겠습니다.

extension 등록

@ExtendWith(ClearExtension.class)
@Import(ClearDatabase.class)
@SpringBootTest
public abstract class IntegrationTestContainers {
	...
}
  • @ExtendWith을 통해 위에서 구현한 extension을 사용하도록 하였습니다. 이를 통해 테스트를 진행하게 되면 자동으로 Extension에서 구현한 메소드가 실행되게 됩니다.
  • @Import저의 경우 @TestComponent를 사용하고 있기에 이를 사용할 수 있도록 하기 위해 @Import로 가져왔습니다.

사용 결과

  • container 생성 로그
  • mongodb data drop 로그

로그를 통해 실제로 mongodb의 데이터가 삭제된 것을 볼 수 있습니다.
(redis의 경우는 사용한 테스트가 없어서 나오지 않았습니다.)

참고자료

junit5
토리맘의 한글라이즈 프로젝트
테스트별로 DB 초기화하기 - 주노
테스트 컨테이너를 사용하는 이유 + 테스트 격리

profile
greenTea입니다.

0개의 댓글