본문 바로가기
📚Framework & Library/JUnit

[Junit] Elasticsearch Unit(Integration) Test Code 작성하기

by inbeom 2024. 1. 23.
728x90
Spring에서 Junit을 사용하여 Elasticsearch 테스트 코드를 작성하는 방법

 

 

Spring 프로젝트에서 Junit으로 Test Code를 작성할 때 RDB(Mysql, Postgresql, Orcle 등)는 JPA나 MyBatis의 Queyr(method)를 사용하여 결과값을 간단히 검증할 수 있지만 Elasticsearch의 경우 org.elasticsearch에서 제공하는 Library를 활용하여 TEST CODE를 효율적으로 작성할 수 있다.

 

기존에는 ES 임베디드 환경을 사용하여 테스트가 가능했지만 7.0 버전부터 사용이 불가능해진 것으로 보이며 ES의 개발자가 대안으로 RestHighLevelClient를 사용한 In Memory 테스트 방법을 추천한다.

https://discuss.elastic.co/t/es-embedded-in-7-0/219823

 

ES Embedded in 7.0

Is it possible to run ES embedded in 7.0? I am running into the same situation described in this previous thread. One of the participants mentioned that running ES embedded is deprecated in 7.0. I didn't see any mention of this in the release notes or brea

discuss.elastic.co

 

Github (source code)

https://github.com/joel-costigliola/elastic-search-test/blob/master/src/test/java/es/example/test/integration/EsIntegrationTest.java

 

 

통합(Integration) 테스트 예시

/**
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * Copyright 2017 the original author or authors.
 */
package es.example.test.integration;

import static org.assertj.core.api.BDDAssertions.then;

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
import org.elasticsearch.test.ESIntegTestCase.Scope;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;

@ClusterScope(scope = Scope.SUITE) // TestSuite에 대한 Elasticsearch 클러스터의 범위 지정
@ThreadLeakScope(ThreadLeakScope.Scope.NONE) // 테스트 중에 Thread 누수가 없어야 함을 지정
@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
public class EsIntegrationTest extends ESIntegTestCase {

	private static final String TRACE_ID_FIELD = "traceID";
	private static final String TRACE_TYPE = "trace";
	private static final String OPEN_TRACE_INDEX = "traces";
	private static final String OPERATION_NAME_FIELD = "operationName";

	private Client client;

	@Override
	@Before
	public void setUp() throws Exception { // setup 함수를 override하여 client 세팅
		super.setUp();
		this.client = client();
	}

	// ESIntegTestCase는 자체 AssertThat을 정의하는 junit Assert에서 상속되므로 
       // AssertJ AssertThat을 사용할 수 없지만 BDDAssertions.then을 사용할 수 있다. :)

	@Test //Todo. operationName 기준 TEST CASE
	public void search_open_tracing_traces_by_operationName() throws Exception {
		// GIVEN
		final String operationName = randomOperationName();
		indexOpenTraceDocument(randomTraceID(), operationName);
		indexOpenTraceDocument(randomTraceID(), operationName);

		// WHEN
		final SearchResponse response = searchOpenTracesByOperationName(operationName);

		// THEN
		then(response.getHits())
				.hasSize(2)
				.extracting(SearchHit::getSourceAsMap)
				.allSatisfy(hit -> then(hit).containsEntry(OPERATION_NAME_FIELD, operationName));
	}

	@Test //Todo. traceID 기준 TEST CASE
	public void search_open_tracing_traces_by_traceID() throws Exception {
		// GIVEN
		final String traceID = randomTraceID();
		indexOpenTraceDocument(traceID, randomOperationName());
		indexOpenTraceDocument(traceID, randomOperationName());

		// WHEN
		final SearchResponse response = searchOpenTracesByTraceID(traceID);

		// THEN
		then(response.getHits())
				.hasSize(2)
				.extracting(SearchHit::getSourceAsMap)
				.allSatisfy(hit -> then(hit).containsEntry(TRACE_ID_FIELD, traceID));
	}

	private String randomTraceID() {
		return randomAlphaOfLengthBetween(1, 50);
	}

	private String randomOperationName() {
		return randomAlphaOfLengthBetween(1, 50);
	}

	// operationName으로 데이터 조회
	private SearchResponse searchOpenTracesByOperationName(final String operationName) {
		return this.client.prepareSearch(OPEN_TRACE_INDEX)
				.setTypes(TRACE_TYPE)
				.setQuery(QueryBuilders.commonTermsQuery(OPERATION_NAME_FIELD, operationName))
				.get();
	}

	// traceID로 데이터 조회
	private SearchResponse searchOpenTracesByTraceID(final String traceID) {
		return this.client.prepareSearch(OPEN_TRACE_INDEX)
				.setTypes(TRACE_TYPE)
				.setQuery(QueryBuilders.commonTermsQuery(TRACE_ID_FIELD, traceID))
				.get();
	}

    // 초기 Test 데이터 생성 함수.
	private void indexOpenTraceDocument(final String traceID, final String operationName) throws Exception {
		this.client.prepareIndex(OPEN_TRACE_INDEX, TRACE_TYPE)
				.setSource(generateOpenTrace(traceID, operationName), XContentType.JSON)
				.execute()
				.get();
		// refreshes the index otherwise we would not find anything
		refresh();
	}

    // 테스트 데이터 반환
	private static String generateOpenTrace(final String traceID, final String operationName) {
		return "{\n" +
				"   \"traceID\": \"" + traceID + "\",\n" +
				"   \"spanID\": \"3b1237777ef2d83\",\n" +
				"   \"parentSpanID\": \"bbe20e919b94f710\",\n" +
				"   \"operationName\": \"" + operationName + "\",\n" +
				"   \"references\": [],\n" +
				"   \"startTime\": 1510878645507000,\n" +
				"   \"duration\": 129000,\n" +
				"   \"tags\": [\n" +
				"     {\n" +
				"       \"key\": \"mvc.controller.class\",\n" +
				"       \"type\": \"string\",\n" +
				"       \"value\": \"Apis\"\n" +
				"     },\n" +
				"     {\n" +
				"       \"key\": \"mvc.controller.method\",\n" +
				"       \"type\": \"string\",\n" +
				"       \"value\": \"pong\"\n" +
				"     },\n" +
				"     {\n" +
				"       \"key\": \"source\",\n" +
				"       \"type\": \"string\",\n" +
				"       \"value\": \"KevinWasPong\"\n" +
				"     },\n" +
				"     {\n" +
				"       \"key\": \"spring.instance_id\",\n" +
				"       \"type\": \"string\",\n" +
				"       \"value\": \"172.20.41.251:Service2:18081\"\n" +
				"     },\n" +
				"     {\n" +
				"       \"key\": \"span.kind\",\n" +
				"       \"type\": \"string\",\n" +
				"       \"value\": \"server\"\n" +
				"     }\n" +
				"   ],\n" +
				"   \"logs\": [],\n" +
				"   \"processID\": \"\",\n" +
				"   \"process\": {\n" +
				"     \"serviceName\": \"service2\",\n" +
				"     \"tags\": [\n" +
				"       {\n" +
				"         \"key\": \"ip\",\n" +
				"         \"type\": \"int64\",\n" +
				"         \"value\": \"-1407964677\"\n" +
				"       }\n" +
				"     ]\n" +
				"   },\n" +
				"   \"warnings\": null,\n" +
				"   \"startTimeMillis\": 1510878645507\n" +
				" }";
	}
}

 

※Test Code 설명

ESIntegTestCase 확장:
 해당 클래스는 Elasticsearch의 통합 테스트를 위해 제공되는 ESIntegTestCase를 확장한다.
설정 메서드:
 setUp 메서드가 Elasticsearch 클라이언트를 초기화하기 위해 override한다.
테스트 메서드:
 searchOpenTracesByOperationName 및 searchOpenTracesByTraceID 메서드는 각각 operationName, traceID를 기반 으로 검색을 수행하고, AssertJ의 BDDAssertions를 사용하여 검색 결과에 대한 어설션을 수행한다.
문서 인덱싱:
 indexOpenTraceDocument 메서드는 Spring Data Elasticsearch 클라이언트를 사용하여 Elasticsearch에 open trace 문서를 인덱싱합니다.
데이터 생성:
 generateOpenTrace 메서드는 traceID, spanID, operationName 및 tag와 같은 다양한 필드를 포함하는 open trace을 나타내는 샘플 JSON 문서를 생성한다.
라이선스 정보:
 코드는 Apache License, Version 2.0에 따라 라이선스가 부여되었음을 시작 부분에 명시한다.

 


 

유닛(Unit) 테스트 예시

/**
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * Copyright 2017 the original author or authors.
 */
package es.example.test.unit;

import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
public class EsUnitTest extends ESTestCase {

    @Test
    public void simpleEsTest() {
        // GIVEN
        String indexName = "example_index";
        String documentId = "1";
        String documentSource = "{\"field\": \"value\"}";

        // WHEN
        IndexRequest indexRequest = new IndexRequest(indexName)
                .id(documentId)
                .source(documentSource, XContentType.JSON);
        
        IndexResponse indexResponse = client().index(indexRequest, COMMON_OPTIONS);

        // THEN
        assertNotNull(indexResponse);
        assertEquals("created", indexResponse.getResult().name());
        assertTrue(indexResponse.getVersion() > 0);
    }
}

 

 

 

 

 - 끝 - 

728x90