17 Şubat 2021 Çarşamba

WireMock Kullanımı - Sunucudan Gelen Cevap Gibidir

Giriş
WireMock yerine bir diğer seçenek MockServer

Açıklaması şöyle
... sets up a mock server locally and allows to easily define requests and responses.
Açıklaması şöyle
WireMock is a great API mocking library. It is written in Java and used mostly by Java developers. But because it relies on easy to use configuration, it can be used by anyone who has to mock API.

WireMock allows you not also to stub responses by endpoint URL, you can also put it between the client and your existing API. With proxying some of the requests can be still handled by your API server, while others are mocked by WireMock.
Şu satırı dahil ederiz
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
Eğer testlerde  bir tane gerçek bean yerine test bean kullanmak istersek şöyle yaparız
spring:
  main:
    allow-bean-definition-overriding: true
Maven
Örnek
Şöyle yaparız
<dependency>
  <groupId>com.github.tomakehurst</groupId>
  <artifactId>wiremock</artifactId>
  <version>2.27.2</version>
  <scope>test</scope>
</dependency>
Örnek - Fat Jar
Bu kütüthanenin groupId değeri com.github.tomakehurst yerine artık org.wiremock oldu. Şöyle yaparız
<dependency>
  <groupId>org.wiremock</groupId>
  <artifactId>wiremock-standalone</artifactId>
  <version>2.27.2</version>
  <scope>test</scope>
</dependency>
Örnek - Java 8 Çok Eski Kullanmayın
Şöyle yaparız
<dependency>
  <groupId>com.github.tomakehurst</groupId>
  <artifactId>wiremock-jre8</artifactId>
  <version>2.28.1</version>
  <scope>test</scope>
</dependency>
Gradle
Şöyle yaparız
dependencies {
  testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
}
Kullanım
WireMockServer nesnesi yaratmak gerekir. 
1. Bu nesneyi sarmalayan bir başka sınıf yaratılarak tüm testlere alt yapı oluşturulabilir
2. Junit 5 Extension kullanılabilir

Örnek - Record Playback İle Kullanım
Açıklaması şöyle
WireMock has a feature called Record and Playback. It can create stub mappings automatically from requests it has received. Combined with its proxying feature this allows us to “record” stub mappings from interaction with existing APIs. When in action, we have the option to either call the external API and capture the response or use one of the earlier recorded stubs to reply to the caller without calling the external API.
Şu satırı dahil ederiz
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
import com.github.tomakehurst.wiremock.recording.RecordSpec;
import com.github.tomakehurst.wiremock.recording.RecordingStatus;
Örnek
Elimizde şöyle bir konfigürasyon olsun. Bunu okuyan kendi kodumuz var.
wiremock-config:
  proxies:
    - name: university
      port: 9081
      url: http://universities.hipolabs.com
      recording: true

universitiesBaseURL: http://localhost:9081
Şöyle yaparız. Burada önce gerçek cevap kaydediliyor.
@ActiveProfiles(value = "integration")
@SpringBootTest(classes = SpringBootWiremockApplication.class, 
  webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Slf4j
@AutoConfigureMockMvc
class SpringBootWiremockApplicationTests {
  @Autowired
  private MockMvc mockMvc;
  
  @Autowired
  private WireMockConfig wireMockConfig;

  
  private final List<WireMockServer> servers = new ArrayList<>();


  Function<WireMockProxy, WireMockServer> getMockServer =
    (WireMockProxy proxy) ->
      new WireMockServer(
        WireMockConfiguration.options()
          .port(proxy.getPort())
          .notifier(new ConsoleNotifier(true)));

  @BeforeEach
  void startRecording() {
    List<WireMockProxy> proxies = wireMockConfig.getProxies();
    if (!CollectionUtils.isEmpty(proxies)) {
      for (WireMockProxy proxy : proxies) {
        WireMockServer wireMockServer = getMockServer.apply(proxy);
        wireMockServer.start();
        if (proxy.isRecording()) {
          wireMockServer.startRecording(config(proxy.getUrl(), true));
        }
        servers.add(wireMockServer);
      }
    }
  }

  @AfterEach
  void stopRecording() {
     if (!CollectionUtils.isEmpty(servers)) {
       for (WireMockServer server : servers) {
         if (server.getRecordingStatus().getStatus().equals(RecordingStatus.Recording)) {
           server.stopRecording();
         }
         server.stop();
       }
    }
  }

  private RecordSpec config(String recordingURL, boolean recordingEnabled) {
    return WireMock.recordSpec()
	  .forTarget(recordingURL)
	  .onlyRequestsMatching(RequestPatternBuilder.allRequests())
	  .captureHeader("Accept")
	  .makeStubsPersistent(recordingEnabled)
	  .ignoreRepeatRequests()
	  .matchRequestBodyWithEqualToJson(true, true)
	  .build();
  }

}

Örnek - Unit Test İle Kullanım
Test başında @BeforeEach veya @BeforeAll ile sunucuyu başlatırız. Şöyle yaparız
@BeforeEach
void setUp() {
  server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
  server.start();
}
Test sonunda @AfterEach veya @AfterAll ile sunucuyu kapatırız. Şöyle yaparız
@AfterEach
void tearDown() {
  server.shutdownServer();
}
Örnek - JUnit 5 Extension
Şöyle bir JUnit 5 extension olsun
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;

public class WireMockExtension
        implements AfterAllCallback, AfterEachCallback, ParameterResolver {

  private WireMockServer wiremockServer;

  @Override
  public void afterAll(ExtensionContext context) {
    wiremockServer.shutdown();
  }

  @Override
  public void afterEach(ExtensionContext context) {
    wiremockServer.resetAll();
  }

  @Override
  public WireMockServer resolveParameter(ParameterContext parameterContext, 
    ExtensionContext arg1) 
  throws ParameterResolutionException {

    wiremockServer = new WireMockServer(options()
      .notifier(new ConsoleNotifier(true))
      .dynamicPort());
    wiremockServer.start();
    return wiremockServer;
  }

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, 
    ExtensionContext context) 
  throws ParameterResolutionException {
    return
      parameterContext.getParameter().getType().equals(WireMockServer.class)
      && parameterContext.isAnnotated(WireMockInstance.class);
    }
}
Şöyle bir anotasyonumuz olsun
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface WireMockInstance {

}
Test sınıfının iskeleti şöyle olsun
@ExtendWith({WireMockExtension.class, SpringExtension.class})
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {
                "logging.level.root=INFO",
                "zephyr.api.username=zephyrTestUser",
                "zephyr.api.password=zephyrTestPassword",
                "slack.app.oauth.accessToken=testFakeToken"
        })
@ContextConfiguration(initializers = AppIntegrationTestPropertyInitializer.class)
public class IntegrationTest {

}
//Because WireMock servers start on random ports, we need to set up our Jira 
//and Slack API base URLs on the fly. 
//This is done using initializers in @ContextConfiguration
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.support.TestPropertySourceUtils;

class AppIntegrationTestPropertyInitializer implements 
  ApplicationContextInitializer<ConfigurableApplicationContext> {

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
      "zephyr.api.baseUrl="
        + String.format("%s/jira/rest/zapi/latest",
                        IntegrationTest.zephyrWiremockServer.baseUrl()));

    TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
      "slack.baseUrl="
        + String.format("%s/api",
                         IntegrationTest.slackWiremockServer.baseUrl()));
    }
}
BeforeAll ve AfterEach şöyle olsun
@BeforeAll
public static void beforeAll(@WireMockInstance WireMockServer server1, @WireMockInstance WireMockServer server2) {
  zephyrWiremockServer = server1;
  slackWiremockServer = server2;

  zephyrWiremockServer.addMockServiceRequestListener(new RequestListener() {
    Override
    public void requestReceived(Request request, Response response) {
      // if we would fail directly here in listener, JUnit error would be really hard to understand (as no response would be generated)
      // therefore saving the value to assert it later in main test flow
      isZephyrAuthSingleValued = request.getHeaders().getHeader("Authorization").isSingleValued();
    }
  });

  slackWiremockServer.addMockServiceRequestListener(new RequestListener() {
    @Override
    public void requestReceived(Request request, Response response) {
      // if we would fail directly here in listener, JUnit error would be really hard to understand (as no response would be generated)
      // therefore saving the value to assert it later in main test flow
      isSlackAuthSingleValued = request.getHeaders().getHeader("Authorization").isSingleValued();
     }
   });
  }

@AfterEach
public void afterEach() {
  assertTrue(isZephyrAuthSingleValued,
    "There must be only one 'Authorization' header in all Zephyr requests");
  assertTrue(isSlackAuthSingleValued,
    "There must be only one 'Authorization' header in all Slack requests");

  zephyrWiremockServer.verify(anyRequestedFor(anyUrl())
    .withBasicAuth(new BasicCredentials("zephyrTestUser", "zephyrTestPassword")));

  slackWiremockServer.verify(anyRequestedFor(anyUrl())
    .withHeader("Authorization", equalTo("Bearer testFakeToken")));
}
@Test
public void testAuthenticatedCallToJiraAndSlack() throws IOException {
  // mock Zephyr API response:
  String zephyrResponse = getResourceFileContent("IntegrationTest/testAuthenticatedCallToJiraAndSlack/zephyr_list_executions.json");
  zephyrWiremockServer.stubFor(get(urlEqualTo("/jira/rest/zapi/latest/execution?issueId=5112096"))
    willReturn(aResponse()
    .withStatus(200)
    .withHeader("Content-Type", "application/json;charset=utf-8")
    .withBody(zephyrResponse)));

  // mock Slack API response:
  String slackResponse = getResourceFileContent("IntegrationTest/testAuthenticatedCallToJiraAndSlack/slack_response.json");
  slackWiremockServer.stubFor(post(urlEqualTo("/api/chat.postMessage"))
    .withRequestBody(matchingJsonPath("$[?(@.channel == 'testing-channel')]"))
    .willReturn(aResponse()
    .withStatus(200)
    .withHeader("Content-Type", "application/json;charset=utf-8")
    .withBody(slackResponse)));


  // test
  String result = controller.getResource();

  // verify
  assertEquals("OK result", result);
}

private String getResourceFileContent(String resourceName) throws IOException {
  URL url = Resources.getResource(resourceName);
  return Resources.toString(url, StandardCharsets.UTF_8);
}

WireMock API

WireMockServer Sınıfı
WireMockServer Sınıfı yazısına taşıdım

Hiç yorum yok:

Yorum Gönder