HTTP-Integrationstests

Zeigt den Integrationstest einer HTTP-Funktion.

Dokumentationsseiten mit diesem Codebeispiel

Die folgenden Dokumente enthalten das Codebeispiel im Kontext:

Codebeispiel

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>

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.File;
import java.io.IOException;
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 {
  // 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:8080";

  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("basedir");

    // 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 {
    // Terminate the running Functions Framework Maven plugin process
    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
    RetryRegistry registry = RetryRegistry.of(RetryConfig.custom()
        .maxAttempts(8)
        .intervalFunction(IntervalFunction.ofExponentialBackoff(200, 2))
        .retryExceptions(IOException.class)
        .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 execPromise = require('child-process-promise').exec;
const path = require('path');
const requestRetry = require('requestretry');
const uuid = require('uuid');

const PORT = process.env.PORT || 8080;
const BASE_URL = `http://localhost:${PORT}`;
const cwd = path.join(__dirname, '..');

  let ffProc;

  // Run the functions-framework instance to host functions locally
  before(() => {
    // exec's 'timeout' param won't kill children of "shim" /bin/sh process
    // Workaround: include "& sleep <TIMEOUT>; kill $!" in executed command
    ffProc = execPromise(
      `functions-framework --target=helloHttp --signature-type=http --port ${PORT} & sleep 2; kill $!`,
      {shell: true, cwd}
    );
  });

  after(async () => {
    // Wait for the functions framework to stop
    await ffProc;
  });

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

    const response = await requestRetry({
      url: `${BASE_URL}/helloHttp`,
      method: 'POST',
      body: {name},
      retryDelay: 200,
      json: true,
    });

    assert.strictEqual(response.statusCode, 200);
    assert.strictEqual(response.body, `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()

Nächste Schritte

Informationen zum Suchen und Filtern von Codebeispielen für andere Google Cloud-Produkte finden Sie im Google Cloud-Beispielbrowser