Tests d'intégration HTTP

Cette page fournit un exemple de test d'intégration d'une fonction HTTP.

Pages de documentation incluant cet exemple de code

Pour afficher l'exemple de code utilisé en contexte, consultez la documentation suivante :

Exemple de code

C#

using Google.Cloud.Functions.Testing;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace HelloHttp.Tests
{
    public class FunctionIntegrationTest
    {
        [Fact]
        public async Task GetRequest_NoParameters()
        {
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "uri");
            string text = await ExecuteRequest(request);
            Assert.Equal("Hello world!", text);
        }

        [Fact]
        public async Task GetRequest_UrlParameters()
        {
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "uri?name=Cho");
            string text = await ExecuteRequest(request);
            Assert.Equal("Hello Cho!", text);
        }

        [Fact]
        public async Task PostRequest_BodyParameters()
        {
            string json = "{\"name\":\"Julie\"}";
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "uri")
            {
                Content = new StringContent(json)
            };
            string text = await ExecuteRequest(request);
            Assert.Equal("Hello Julie!", text);
        }

        /// <summary>
        /// Executes the given request in the function in an in-memory test server,
        /// validates that the response status code is 200, and returns the text of the
        /// response body. FunctionTestServer{T} is provided by the
        /// Google.Cloud.Functions.Testing package.
        /// </summary>
        private static async Task<string> ExecuteRequest(HttpRequestMessage request)
        {
            using (var server = new FunctionTestServer<Function>())
            {
                using (HttpClient client = server.CreateClient())
                {
                    HttpResponseMessage response = await client.SendAsync(request);
                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }
    }
}

C++

#include <boost/filesystem.hpp>
#include <boost/process.hpp>
#include <curl/curl.h>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <thread>

namespace {

namespace bp = boost::process;
namespace bfs = boost::filesystem;  // Boost.Process cannot use std::filesystem

struct HttpResponse {
  long code;  // NOLINT(google-runtime-int)
  std::string payload;
};

// Wait until an HTTP server starts responding.
bool WaitForServerReady(std::string const& url);
HttpResponse HttpGet(std::string const& url, std::string const& payload);

char const* argv0 = nullptr;

auto ExePath(bfs::path const& filename) {
  static auto const kPath = std::vector<bfs::path>{
      bfs::canonical(argv0).make_preferred().parent_path()};
  return bp::search_path(filename, kPath);
}

class HttpIntegrationTest : public ::testing::Test {
 protected:
  void SetUp() override {
    auto const* base_url = std::getenv("BASE_URL");
    if (base_url != nullptr) {
      url_ = base_url;
      return;
    }
    curl_global_init(CURL_GLOBAL_ALL);
    auto server = bp::child(ExePath("http_integration_server"), "--port=8030");
    url_ = "http://localhost:8030";
    ASSERT_TRUE(WaitForServerReady(url_));
    process_ = std::move(server);
  }

  void TearDown() override {
    if (process_.has_value()) {
      process_->terminate();
      process_->wait();
    }
    curl_global_cleanup();
  }

  [[nodiscard]] std::string const& url() const { return url_; }

 private:
  std::optional<bp::child> process_;
  std::string url_;
};

TEST_F(HttpIntegrationTest, Basic) {
  auto constexpr kOkay = 200;

  auto actual = HttpGet(url(), R"js({"name": "Foo"})js");
  EXPECT_EQ(actual.code, kOkay);
  EXPECT_EQ(actual.payload, "Hello Foo!");

  actual = HttpGet(url(), R"js({})js");
  EXPECT_EQ(actual.code, kOkay);
  EXPECT_EQ(actual.payload, "Hello World!");
}

extern "C" size_t CurlOnWriteData(char* ptr, size_t size, size_t nmemb,
                                  void* userdata) {
  auto* buffer = reinterpret_cast<std::string*>(userdata);
  buffer->append(ptr, size * nmemb);
  return size * nmemb;
}

HttpResponse HttpGet(std::string const& url, std::string const& payload) {
  using CurlHandle = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>;

  auto easy = CurlHandle(curl_easy_init(), curl_easy_cleanup);

  auto setopt = [h = easy.get()](auto opt, auto value) {
    if (auto e = curl_easy_setopt(h, opt, value); e != CURLE_OK) {
      std::ostringstream os;
      os << "error [" << e << "] setting curl_easy option <" << opt
         << ">=" << value;
      throw std::runtime_error(std::move(os).str());
    }
  };
  auto get_response_code = [h = easy.get()]() {
    long code;  // NOLINT(google-runtime-int)
    auto e = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &code);
    if (e == CURLE_OK) {
      return code;
    }
    throw std::runtime_error("Cannot get response code");
  };

  setopt(CURLOPT_URL, url.c_str());
  setopt(CURLOPT_POSTFIELDSIZE, payload.size());
  setopt(CURLOPT_POSTFIELDS, payload.data());
  setopt(CURLOPT_WRITEFUNCTION, &CurlOnWriteData);
  std::string buffer;
  setopt(CURLOPT_WRITEDATA, &buffer);

  auto e = curl_easy_perform(easy.get());
  if (e == CURLE_OK) {
    return HttpResponse{get_response_code(), std::move(buffer)};
  }
  return HttpResponse{-1, {}};
}

bool WaitForServerReady(std::string const& url) {
  using namespace std::chrono_literals;
  auto constexpr kOkay = 200;
  for (auto delay : {100ms, 200ms, 400ms, 800ms, 1600ms}) {  // NOLINT
    std::cout << "Waiting for server to start [" << delay.count() << "ms]\n";
    std::this_thread::sleep_for(delay);
    try {
      auto r = HttpGet(url, "{}");
      if (r.code == kOkay) return true;
      std::cerr << "... [" << r.code << "]" << std::endl;
    } catch (std::exception const& ex) {
      // The HttpEvent() function may fail with an exception until the server is
      // ready. Log it to ease troubleshooting in the CI builds.
      std::cerr << "WaitForServerReady[" << delay.count()
                << "ms]: server ping failed with " << ex.what() << std::endl;
    }
  }
  return false;
}

}  // namespace

int main(int argc, char* argv[]) {
  ::testing::InitGoogleMock(&argc, argv);
  ::argv0 = argv[0];
  return RUN_ALL_TESTS();
}

Java


import static com.google.common.truth.Truth.assertThat;

import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ExampleIT {
  // Each function must be assigned a unique port to run on.
  // Otherwise, tests can flake when 2+ functions run simultaneously.
  // This is also specified in the `function-maven-plugin` config in `pom.xml`.
  private static final int PORT = 8081;

  // Root URL pointing to the locally hosted function
  // The Functions Framework Maven plugin lets us run a function locally
  private static final String BASE_URL = "http://localhost:" + PORT;

  private static Process emulatorProcess = null;
  private static HttpClient client = HttpClient.newHttpClient();

  @BeforeClass
  public static void setUp() throws IOException {
    // Get the sample's base directory (the one containing a pom.xml file)
    String baseDir = System.getProperty("user.dir");

    // Emulate the function locally by running the Functions Framework Maven plugin
    emulatorProcess = new ProcessBuilder()
        .command("mvn", "function:run")
        .directory(new File(baseDir))
        .start();
  }

  @AfterClass
  public static void tearDown() throws IOException {
    // Display the output of the plugin process
    InputStream stdoutStream = emulatorProcess.getInputStream();
    ByteArrayOutputStream stdoutBytes = new ByteArrayOutputStream();
    stdoutBytes.write(stdoutStream.readNBytes(stdoutStream.available()));
    System.out.println(stdoutBytes.toString(StandardCharsets.UTF_8));

    // Terminate the running Functions Framework Maven plugin process
    if (emulatorProcess.isAlive()) {
      emulatorProcess.destroy();
    }
  }

  @Test
  public void helloHttp_shouldRunWithFunctionsFramework() throws Throwable {
    String functionUrl = BASE_URL + "/helloHttp";

    HttpRequest getRequest = HttpRequest.newBuilder().uri(URI.create(functionUrl)).GET().build();

    // The Functions Framework Maven plugin process takes time to start up
    // Use resilience4j to retry the test HTTP request until the plugin responds
    // See `retryOnResultPredicate` here: https://resilience4j.readme.io/docs/retry
    RetryRegistry registry = RetryRegistry.of(RetryConfig.custom()
        .maxAttempts(12)
        .intervalFunction(IntervalFunction.ofExponentialBackoff(200, 2))
        .retryExceptions(IOException.class)
        .retryOnResult(body -> body.toString().length() == 0)
        .build());
    Retry retry = registry.retry("my");

    // Perform the request-retry process
    String body = Retry.decorateCheckedSupplier(retry, () -> client.send(
        getRequest,
        HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).body()
    ).apply();

    // Verify the function returned the right results
    assertThat(body).isEqualTo("Hello world!");
  }
}

Node.js

const assert = require('assert');
const {exec} = require('child_process');
const {request} = require('gaxios');
const uuid = require('uuid');
const waitPort = require('wait-port');

const PORT = process.env.PORT || 8080;
const BASE_URL = `http://localhost:${PORT}`;

  let ffProc;

  // Run the functions-framework instance to host functions locally
  before(async () => {
    ffProc = exec(
      `npx functions-framework --target=helloHttp --signature-type=http --port ${PORT}`
    );
    await waitPort({host: 'localhost', port: PORT});
  });

  after(() => ffProc.kill());

  it('helloHttp: should print a name', async () => {
    const name = uuid.v4();

    const response = await request({
      url: `${BASE_URL}/helloHttp`,
      method: 'POST',
      data: {name},
    });

    assert.strictEqual(response.status, 200);
    assert.strictEqual(response.data, `Hello ${name}!`);
  });

Python

import os
import subprocess
import uuid

import requests
from requests.packages.urllib3.util.retry import Retry

def test_args():
    name = str(uuid.uuid4())
    port = os.getenv('PORT', 8080)  # Each functions framework instance needs a unique port

    process = subprocess.Popen(
      [
        'functions-framework',
        '--target', 'hello_http',
        '--port', str(port)
      ],
      cwd=os.path.dirname(__file__),
      stdout=subprocess.PIPE
    )

    # Send HTTP request simulating Pub/Sub message
    # (GCF translates Pub/Sub messages to HTTP requests internally)
    BASE_URL = f'http://localhost:{port}'

    retry_policy = Retry(total=6, backoff_factor=1)
    retry_adapter = requests.adapters.HTTPAdapter(
      max_retries=retry_policy)

    session = requests.Session()
    session.mount(BASE_URL, retry_adapter)

    name = str(uuid.uuid4())
    res = requests.post(
      BASE_URL,
      json={'name': name}
    )
    assert res.text == 'Hello {}!'.format(name)

    # Stop the functions framework process
    process.kill()
    process.wait()

Étape suivante

Pour rechercher et filtrer des exemples de code pour d'autres produits Google Cloud, consultez l'exemple de navigateur Google Cloud.