'stub'에 해당되는 글 2건

  1. 2020.05.03 :: gRPC - java gRPC 간단한 사용법
  2. 2019.09.09 :: TDD - 단위 테스트 목(Mock)객체 사용
Web/gRPC 2020. 5. 3. 14:02

이번 포스팅은 gRPC의 세세한 기능을 다루기 이전에 간단하게 java로 gRPC 서버와 클라이언트 코드를 작성해보고 감을 익혀보는 포스팅이다. 오늘 구성할 프로젝트 구조는 아래와 같다.

 

  1. grpc-common : .proto 파일을 이용하여 client와 server가 공통으로 사용할 소스를 generate한 프로젝트. client와 server에서 submodule로 추가할 프로젝트이다.
  2. grpc-server : server역할을 할 gRPC 서버 애플리케이션이다.
  3. grpc-client : client역할을 할 gRPC 클라이언트 애플리케이션이다.

 

grpc-common

모든 소스코드는 아래 깃헙 주소를 참고하면 된다.

 

 

yoonyeoseong/grpc-common

Contribute to yoonyeoseong/grpc-common development by creating an account on GitHub.

github.com

우선 gradle 프로젝트를 생성해준다. 그 이후 build.gradle을 아래와 같이 세팅한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}
 
plugins {
    id 'java'
}
 
apply plugin: 'com.google.protobuf'
 
group 'org.example'
version '1.0-SNAPSHOT'
 
repositories {
    mavenCentral()
}
 
dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'
 
    testCompile group: 'junit', name: 'junit', version: '4.12'
}
 
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.7.1"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}
 
cs

 

gRPC를 위한 의존서을 추가해주었고, .proto 파일에 작성된 리소스를 자바코드로 generate하기 위한 설정들이 들어가있다. 

 

이제 src/main/proto 디렉토리를 하나 생성해준다. 그 이후에 아래와 같은 .proto 파일을 작성해준다.

 

syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "SampleProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

message SampleRequest {
  string userId = 1;
  string message = 2;
}

message SampleResponse {
  string message = 1;
}

service SampleService {
  rpc SampleCall (SampleRequest) returns (SampleResponse) {}
}

 

아직은 해당 파일에 대한 세세한 내용은 다루지 않는다. 추후에 다루어 볼것이니 일단 몰라도 넘어가자. proto 파일을 생성해주었으면 generateProto task를 실행시켜준다. 이후 소스가 잘 생성되었는지 확인하자.

 

 

grpc-server

 

 

yoonyeoseong/grpc-server

Contribute to yoonyeoseong/grpc-server development by creating an account on GitHub.

github.com

 

이제는 grpc 서버 역할을 할 애플리케이션을 작성한다. 우선 바로 위에서 생성한 프로젝트를 submodule로 추가해준다.

 

> git submodule add <grpc-common 깃 주소>

 

필자는 spring의 injection을 사용하기 위해서 스프링부트 프로젝트로 생성해주었다. 일반 gradle 프로젝트도 상관없으니 편할대로 생성해주면 될듯하다.

 

build.gradle에 grpc-common에 생성된 소스를 sourceSets로 넣는 설정을 하나 넣어준다. 그리고 필요한 dependency를 추가한다. 물론 grpc-server에 필요하지 않은 의존성이 있긴하지만 우선 추가하자.

 

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

apply plugin: 'com.google.protobuf'

group = 'com.levi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
}

test {
    useJUnitPlatform()
}

sourceSets {
    main {
        java {
            srcDirs += [
                    'grpc-common/build/generated/source/proto/main/grpc',
                    'grpc-common/build/generated/source/proto/main/java'
            ]
        }
    }
}

 

이제 우리가 정의한 service를 구현해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Service
public class SampleServiceImpl extends SampleServiceGrpc.SampleServiceImplBase {
 
    @Override
    public void sampleCall(SampleRequest request, StreamObserver<SampleResponse> responseObserver) {
        log.info("SampleServiceImpl#sampleCall - {}, {}", request.getUserId(), request.getMessage());
        SampleResponse sampleResponse = SampleResponse.newBuilder()
                .setMessage("grpc service response")
                .build();
 
        responseObserver.onNext(sampleResponse);
        responseObserver.onCompleted();
    }
}
cs

 

단순히 우리가 정의한 service(sampleCall)을 구현하는 것 뿐이다. 요청으로 SampleRequest 객체를 받고, 응답으로 SampleResponse를 내보내주면 된다. 비즈니스 로직은 없고 단순히 로그를 찍고 SampleResponse를 리턴하는 단순한 로직이다. 사실 client-server 통신 방법은 크게 4가지가 있는데, 4가지중 "client : server = 1 : 1" 로 통신하는 구조로 만들었다.

 

gRPC 서버를 구동하기 위한 클래스를 하나 작성한다. 뭐 여러가지 튜닝할 요소가 있을지는 모르겠지만, 우선 띄우는 것에 집중하자. gRPC-server로 결국은 server이다. 포트를 할당 받아서 서버형태로 띄운다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class GrpcRunner implements ApplicationRunner {
 
    private static final int PORT = 3030;
    private static final Server SERVER = ServerBuilder.forPort(PORT)
            .addService(new SampleServiceImpl())
            .build();
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        SERVER.start();
    }
}
cs

 

이제 spring application을 실행시키면 grpc server가 구동된다.

 

grpc-client

 

 

yoonyeoseong/grpc-client

Contribute to yoonyeoseong/grpc-client development by creating an account on GitHub.

github.com

 

마지막으로 grpc-server와 통신할 grpc-client이다. grpc-common 프로젝트를 submodule로 추가한다.

 

> git submodule add <grpc-common 깃 주소>

 

이제 build.gradle을 작성하자. grpc-server와 같이 필요없는 의존성이 있을 수 있지만 일단 추가하자.

 

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

apply plugin: 'com.google.protobuf'

group = 'com.levi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
}

sourceSets {
    main {
        java {
            srcDirs += [
                    'grpc-common/build/generated/source/proto/main/grpc',
                    'grpc-common/build/generated/source/proto/main/java'
            ]
        }
    }
}

test {
    useJUnitPlatform()
}

 

이제 stub 객체를 이용하여 grpc-server의 원격 메서드를 호출하는 client 코드를 간단하게 작성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Service
public class GrpcClient {
    private static final int PORT = 3030;
    public static final String HOST = "localhost";
    private final SampleServiceGrpc.SampleServiceStub asyncStub = SampleServiceGrpc.newStub(
            ManagedChannelBuilder.forAddress(HOST, PORT)
            .usePlaintext()
            .build()
    );
 
    public String sampleCall() {
        final SampleRequest sampleRequest = SampleRequest.newBuilder()
                .setUserId("levi.yoon")
                .setMessage("grpc request")
                .build();
 
        asyncStub.sampleCall(sampleRequest, new StreamObserver<SampleResponse>() {
            @Override
            public void onNext(SampleResponse value) {
                log.info("GrpcClient#sampleCall - {}", value);
            }
 
            @Override
            public void onError(Throwable t) {
                log.error("GrpcClient#sampleCall - onError");
            }
 
            @Override
            public void onCompleted() {
                log.info("GrpcClient#sampleCall - onCompleted");
            }
        });
        return "string";
    }
}
cs

 

우선 client에서는 크게 2가지가 등장한다. (Stub & Channel)

 

  1. Stub : Remote procedure call을 하기 위한 가짜 객체라고 생각하면 된다. 마치 자신의 메서드를 호출하는 듯한 느낌이지만, 실제 원격(grpc-server) 메서드를 호출한다. Stub에는 async, blocking, future등 여러 스텁 종류가 존재하지만, 우리는 asyncStub을 이용한다.
  2. Channel : 결국 Stub도 원격 서버의 메서드를 호출한다. 그렇기 때문에 grpc-server와 통신할 channel을 정의해준다.

 

우리가 정의한 service의 스텁 객체를 생성해주고, 매개변수로 Channel을 생성해서 넣어준다. 우리가 띄울 grpc-server의 host와 port를 넣는다.

 

.proto로 message를 정의하면 java에서 사용할 수 있는 빌더패턴의 메서드, setter&getter등을 만들어준다. 요청을 위한 SampleRequest 객체를 만들었다. 이제 stub의 sampleCall(우리가 정의한 서비스)를 호출한다. 그런데 매개변수로 StreamObserver가 들어가는데, 해당 Stub은 asyncStub이기 때문에 callback 객체를 넣어주는 것이다. 간단히 로그를 찍는 callback 로직을 넣어주었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class GrpcApplication {
 
    private final GrpcClient grpcClient;
 
    public static void main(String[] args) {
        SpringApplication.run(GrpcApplication.class, args);
    }
 
    @GetMapping("/")
    public Mono<String> test() {
        return Mono.just(grpcClient.sampleCall());
    }
 
}
cs

 

위는 간단하게 요청을 받을 controller를 하나 정의하였다.

 

실행

이제 grpc-server와 grpc-client를 띄우고 요청을 보내보자.

 

> curl http://localhost:9090/

#grpc-server
2020-05-03 13:59:18.781  INFO : SampleServiceImpl#sampleCall - levi.yoon, grpc request

#grpc-client
2020-05-03 13:57:09.014  INFO : GrpcClient#sampleCall - message: "grpc service response"
2020-05-03 13:57:09.032  INFO : GrpcClient#sampleCall - onCompleted

 

와우 ! client-server 간의 통신이 잘 이루어졌고, 우리가 찍었던 로그가 잘 보인다. 지금까지 간단하게 java 기반의 grpc를 다루어보았다. 사실 다룰 내용이 훨씬 많다. 그렇지만 이렇게 한번 간단하게 사용해보면 추후에 세세한 부분을 공부할때, 더 수월할 수도 있을 것 같다.

posted by 여성게
:
Web/TDD 2019. 9. 9. 16:41

 

이번 포스팅에서는 목(Mock) 객체를 사용하여 테스트하기 힘든, 혹은 외부환경과 의존성을 끊는 테스트를 하기 위한 방법을 간단하게 다루어 볼 것이다. 여기서 다루는 목(Mock)객체는 정말 단순한 수준의 예제이다. 결과적으로 이번 포스팅의 목적은 목객체는 무엇이고 왜 사용하는 지에 대한 내용이 될 것 같다.

 

지금 진행할 예제는 크게 코드 내용 자체를 알필요?는 없을 것 같다. 이 말은 우리가 개발하며 테스트를 작성할 때, 코드의 내용을 몰라도 된다는 말이 아니다. 테스트 작성은 당연히 코드의 내용을 빠삭히 알고 작성해야 하는 테스크이기 때문이다. 필자가 말하는 "알 필요는 없다"라는 것은 이번 포스팅은 독자들과 같이 애플리케이션을 개발하며 테스트를 작성하는 포스팅이 아니고 어느 순간에 목객체를 사용해야하냐를 다루는 문제이기 때문이다.

 

<위도경도를 이용한 주소정보 받아오기>

아래의 코드는 제목그대로 위도경도를 받아 특정 API를 호출해 해당 위도경도에 위치한 위치정보를 받아오는 코드이다.(효율적인 코드라고는 말할 수 없다. 그냥 흐름만 보자.)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AddressRetriever {
   public Address retrieve(double latitude, double longitude)
         throws IOException, ParseException {
      String parms = String.format("lat=%.6flon=%.6f", latitude, longitude);
      String response = new HttpImpl().get(
        "http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"
        + parms);
 
      JSONObject obj = (JSONObject)new JSONParser().parse(response);
 
      JSONObject address = (JSONObject)obj.get("address");
      String country = (String)address.get("country_code");
      if (!country.equals("us"))
         throw new UnsupportedOperationException(
            "cannot support non-US addresses at this time");
 
      String houseNumber = (String)address.get("house_number");
      String road = (String)address.get("road");
      String city = (String)address.get("city");
      String state = (String)address.get("state");
      String zip = (String)address.get("postcode");
      return new Address(houseNumber, road, city, state, zip);
   }
}
cs

 

위와 같은 코드가 있다. 간단히 코드내용을 설명하면, retrieve 메소드에 위도와 경도 인자를 받아서 해당 인자를 이용하여 특정 API를 호출하고 위치에 대한 정보를 받아오고 있다. 만약 이 메소드를 단위 테스트하기 위해 걸리는 사항이 있나? 필자는 여기서 하나를 꼽자면 "외부 서버에 위치한 API를 호출하는 부분"를 얘기하고 싶다. 이런 부분은 우리가 통제할 수 있는 환경이 아니다. 즉, 우리의 테스트 코드와는 상관없이 API 트래픽이 높아 결과를 받아오는데 시간이 걸릴 수 있고, 최악으로는 API서버가 다운되고 API호출 결과로 예외가 발생할 수 있는 등의 우리가 통제할 수 없는 문제가 발생할 수 있다. 

 

테스트 관점에서 보면, API에서 적절한 결과값을 받아오는 것을 테스트할 수 있지만 "우리가 개발한 로직이 과연 기대한대로 동작하는 가?"를 테스트 해볼 수 있다. 물론 전자는 직접 API를 호출하여 결과값을 받아오는 테스트 코드를 작성할 수 있지만 후자는 굳이 API호출 동작 자체가 필요하지 않을 수 있다. 

 

그렇다면 직접 API를 호출할 필요가 없는 테스트지만 특정 API를 호출하고 있는 로직이 존재하는 코드를 테스트하기 위해서는 어떻게 할까?

이럴때 이용하는 것이 목(Mock) 객체이다. 일종의 스텁객체라고도 볼 수 있을 것 같다. 이러한 객체를 사용하면서 생기는 이점은 무엇일까?

 

  • 외부환경을 신경쓸 필요가 없이 비지니스로직의 성공유무를 테스트할 수 있다.
  • 외부환경에 따라 테스트가 빨리 끝날 수도 있지만, 외부환경이 갑자기 트래픽이 몰리는 시간이라면 테스트가 느리게 끝날 수도 있다. 하지만 목(Mock)객체를 사용한다면 직접 API를 호출하여 결과를 받아오는 것이 아니기 때문에 빠른 테스트 시간을 유지할 수 있다.
  • 테스트의 복잡도를 낮춰준다.

하지만 목객체를 이용하기 전에 위의 코드는 조금 문제점이 있다. API를 호출하는 객체(HttpImpl클래스)가 메서드 내부에서 초기화되고 있다는 것이다. 이것은 목객체를 이용한 테스트 코드를 작성할 수 없다. 우리는 "의존주입"이라는 것을 이용하여 목객체를 이용할 것이기 때문에 간단하게 프로덕 코드의 설계를 변경할 것이다.

 

"의존주입"을 이용하게 되면 생기는 장점은 아래와 같다.

 

  • 해당 클래스의 메서드는 실제 비지니스로직을 수행하는 HttpClient 객체가 목객체인지 혹은 실제 API를 호출하는 객체인지를 알 필요가 없어진다.

특정 오브젝트와의 결합도를 인터페이스(Http라는 인터페이스)를 이용하여 낮춰버렸기 때문에 이 인터페이스의 구현체로 Mock객체를 주입하던 실제 API를 호출하게 되는 HttpClient(HttpImpl 클래스) 객체를 넣던 해당 클래스는 신경쓸 필요가 없어진다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AddressRetriever {
   private Http http;
 
   public AddressRetriever(Http http) {
      this.http = http;
   }
 
   public Address retrieve(double latitude, double longitude) throws IOException, ParseException {
      String parms = String.format("lat=%.6f&lon=%.6f", latitude, longitude);
      String response = http.get("http://open.mapquestapi.com/nominatim/v1/reverse?format=json&" + parms);
 
      JSONObject obj = (JSONObject)new JSONParser().parse(response);
 
      JSONObject address = (JSONObject)obj.get("address");
      String country = (String)address.get("country_code");
      if (!country.equals("us"))
         throw new UnsupportedOperationException(
               "cannot support non-US addresses at this time");
 
      String houseNumber = (String)address.get("house_number");
      String road = (String)address.get("road");
      String city = (String)address.get("city");
      String state = (String)address.get("state");
      String zip = (String)address.get("postcode");
      return new Address(houseNumber, road, city, state, zip);
   }
}
cs

 

위 코드와 같이 특정 API 호출을 담당하는 객체를 인스턴스 변수로 선언하고 생성자에서 의존주입을 하고 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class AddressRetrieverTest {
   @Test
   public void answersAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      Http http = (String url) -> 
         "{\"address\":{"
         + "\"house_number\":\"324\","
         + "\"road\":\"North Tejon Street\","
         + "\"city\":\"Colorado Springs\","
         + "\"state\":\"Colorado\","
         + "\"postcode\":\"80903\","
         + "\"country_code\":\"us\"}"
         + "}";
      AddressRetriever retriever = new AddressRetriever(http);
 
      Address address = retriever.retrieve(38.0,-104.0);
      
      assertThat(address.houseNumber, equalTo("324"));
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
 
   @Test
   public void returnsAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      Http http = new Http() {
         @Override
         public String get(String url) throws IOException {
            return "{\"address\":{"
               + "\"house_number\":\"324\","
               + "\"road\":\"North Tejon Street\","
               // ...
               + "\"city\":\"Colorado Springs\","
               + "\"state\":\"Colorado\","
               + "\"postcode\":\"80903\","
               + "\"country_code\":\"us\"}"
               + "}";
            }};
      AddressRetriever retriever = new AddressRetriever(http);
 
      Address address = retriever.retrieve(38.0,-104.0);
      
      assertThat(address.houseNumber, equalTo("324"));
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
}
cs

 

우리는 설계를 변경한 클래스 테스트 작성을 위와 같이 할 수 있다. (포스팅에 올리지는 않았지만 Http 인터페이스는 get라는 메서드 선언을 하고 있는 FunctionalInterface이다.)

 

테스트에서는 스텁 객체(Http)를 이용해 retrieve 메서드를 테스트하고 있다. 또 다른 방법으로 Mokito라이브러리를 이용한 Mock(목) 객체를 이용하여 테스트를 작성할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AddressRetrieverTest {
   @Test
   public void answersAppropriateAddressForValidCoordinates() 
         throws IOException, ParseException {
      Http http = mock(Http.class);
      when(http.get(contains("lat=38.000000&lon=-104.000000"))).thenReturn(
            "{\"address\":{"
            + "\"house_number\":\"324\","
           // ...
            + "\"road\":\"North Tejon Street\","
            + "\"city\":\"Colorado Springs\","
            + "\"state\":\"Colorado\","
            + "\"postcode\":\"80903\","
            + "\"country_code\":\"us\"}"
            + "}");
      AddressRetriever retriever = new AddressRetriever(http);
 
      Address address = retriever.retrieve(38.0,-104.0);
      
      assertThat(address.houseNumber, equalTo("324"));
      // ...
      assertThat(address.road, equalTo("North Tejon Street"));
      assertThat(address.city, equalTo("Colorado Springs"));
      assertThat(address.state, equalTo("Colorado"));
      assertThat(address.zip, equalTo("80903"));
   }
}
cs

 

목 객체를 이용하면 스텁객체를 사용하는 것보다 더 쉽게 메서드 인자 검사등을 수행할 수 있기 때문에 더 정교하고 똑똑한 일회성 객체를 만들 수 있다.

 

목객체를 이용한 테스트는 비단 외부API를 호출하는 것에만 국한되지 않는다. 기타 비용이 큰 로직에 대해서도 목객체를 이용하여 효율적인 테스트 코드작성, 혹은 비즈니스 로직에 집중된 테스트 코드를 작성할 수 있게된다.

 

하지만 목(Mock) 객체를 이용하기 위해서 중요한 것이 있습니다.

 

  • 목을 사용한 테스트는 진행하길 원하는 내용을 분명하게 기술해야 한다.(메서드 이름등을 명확히 짓는다.)
  • 목이 프로덕 코드의 동작을 올바르게 묘사하고 있는가를 살펴보자.
  • 혹시나 목 객체가 실제 프로덕코드를 실행하고 있지는 않은가?(임시로 프로덕코드에 throw문을 넣어 예외가 발생하는지 테스트 해볼 수 있다.)
  • 프로덕 코드를 직접 적으로 테스트하고 있지 않는 다는 것을 기억하자. 목을 도입하면 테스트 커버리지에서 간극을 형성할 수 있음을 인지하고 실제 클래스의 종단 간 사용성을 보여주는 상위 테스트가 있는지 확인하자.

 

여기까지 간단하게 목객체를 사용하는 이유중 한가지를 다루어봤습니다. 실제 모키토를 이용하여 기술적으로 목객체를 이용하는 방법등을 포스팅 내용에 없기 때문에 별도 자료로 찾아봐야합니다.

posted by 여성게
: