HTTP 関数のテスト

Cloud Functions には 2 つの異なるタイプがあります。CloudEvent 関数とバックグラウンド関数は、いずれも内部 Cloud Platform イベントによってトリガーされるイベント ドリブン関数ですが、HTTP 関数は HTTP リクエストによってトリガーされます。

関数のテスト構造は、その関数で使用される Google Cloud Platform リソースによって決まります。また、関数がどのようにリソースを使用するかは、その関数のトリガー方法によって異なります。

このドキュメントでは、HTTP Cloud Functions のテスト方法について説明します。イベント ドリブン関数のテスト方法については、イベント ドリブン関数のテストをご覧ください。

HTTP でトリガーされる関数

大部分の Cloud Functions 単体テストの場合と同様に、HTTP トリガー関数単体テストには固有の構造があります。システムテストと統合テストは構造が似ていますが、多くの場合、他の関数タイプのテストには当てはまりません。

HTTP 関数の例を次に示します。

Node.js

const escapeHtml = require('escape-html');

/**
 * HTTP Cloud Function.
 *
 * @param {Object} req Cloud Function request context.
 *                     More info: https://expressjs.com/en/api.html#req
 * @param {Object} res Cloud Function response context.
 *                     More info: https://expressjs.com/en/api.html#res
 */
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};

Python

from flask import escape

def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'World'
    return 'Hello {}!'.format(escape(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"encoding/json"
	"fmt"
	"html"
	"net/http"
)

// HelloHTTP is an HTTP Cloud Function with a request parameter.
func HelloHTTP(w http.ResponseWriter, r *http.Request) {
	var d struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	if d.Name == "" {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	fmt.Fprintf(w, "Hello, %s!", html.EscapeString(d.Name))
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Logger;

public class HelloHttp implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HelloHttp.class.getName());

  private static final Gson gson = new Gson();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Check URL parameters for "name" field
    // "world" is the default value
    String name = request.getFirstQueryParameter("name").orElse("world");

    // Parse JSON request and check for "name" field
    try {
      JsonElement requestParsed = gson.fromJson(request.getReader(), JsonElement.class);
      JsonObject requestJson = null;

      if (requestParsed != null && requestParsed.isJsonObject()) {
        requestJson = requestParsed.getAsJsonObject();
      }

      if (requestJson != null && requestJson.has("name")) {
        name = requestJson.get("name").getAsString();
      }
    } catch (JsonParseException e) {
      logger.severe("Error parsing JSON: " + e.getMessage());
    }

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Hello %s!", name);
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace HelloHttp
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            // Check URL parameters for "name" field
            // "world" is the default value
            string name = ((string) request.Query["name"]) ?? "world";

            // If there's a body, parse it as JSON and check for "name" field.
            using TextReader reader = new StreamReader(request.Body);
            string text = await reader.ReadToEndAsync();
            if (text.Length > 0)
            {
                try
                {
                    JsonElement json = JsonSerializer.Deserialize<JsonElement>(text);
                    if (json.TryGetProperty("name", out JsonElement nameElement) &&
                        nameElement.ValueKind == JsonValueKind.String)
                    {
                        name = nameElement.GetString();
                    }
                }
                catch (JsonException parseException)
                {
                    _logger.LogError(parseException, "Error parsing JSON request");
                }
            }

            await context.Response.WriteAsync($"Hello {name}!");
        }
    }
}

Ruby

require "functions_framework"
require "cgi"
require "json"

FunctionsFramework.http "hello_http" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request
  name = request.params["name"] ||
         (JSON.parse(request.body.read)["name"] rescue nil) ||
         "World"
  # Return the response body as a string.
  # You can also return a Rack::Response object, a Rack response array, or
  # a hash which will be JSON-encoded into a response.
  "Hello #{CGI.escape_html name}!"
end

PHP

<?php

use Psr\Http\Message\ServerRequestInterface;

function helloHttp(ServerRequestInterface $request): string
{
    $name = 'World';
    $body = $request->getBody()->getContents();
    if (!empty($body)) {
        $json = json_decode($body, true);
        if (json_last_error() != JSON_ERROR_NONE) {
            throw new RuntimeException(sprintf(
                'Could not parse body: %s',
                json_last_error_msg()
            ));
        }
        $name = $json['name'] ?? $name;
    }
    $queryString = $request->getQueryParams();
    $name = $queryString['name'] ?? $name;

    return sprintf('Hello, %s!', htmlspecialchars($name));
}

単体テスト

次のテストは、上記の HTTP トリガー関数の単体テストとして機能します。

Node.js

Express は、Sinon で疑似的に再現されています。
const assert = require('assert');
const sinon = require('sinon');
const uuid = require('uuid');

const {helloHttp} = require('..');

it('helloHttp: should print a name', () => {
  // Mock ExpressJS 'req' and 'res' parameters
  const name = uuid.v4();
  const req = {
    query: {},
    body: {
      name: name,
    },
  };
  const res = {send: sinon.stub()};

  // Call tested function
  helloHttp(req, res);

  // Verify behavior of tested function
  assert.ok(res.send.calledOnce);
  assert.deepStrictEqual(res.send.firstCall.args, [`Hello ${name}!`]);
});

Python

Flask は、unittest で擬似的に再現されています。
from unittest.mock import Mock

import main

def test_print_name():
    name = 'test'
    data = {'name': name}
    req = Mock(get_json=Mock(return_value=data), args=data)

    # Call tested function
    assert main.hello_http(req) == 'Hello {}!'.format(name)

def test_print_hello_world():
    data = {}
    req = Mock(get_json=Mock(return_value=data), args=data)

    # Call tested function
    assert main.hello_http(req) == 'Hello World!'

Go


package helloworld

import (
	"net/http/httptest"
	"strings"
	"testing"
)

func TestHelloHTTP(t *testing.T) {
	tests := []struct {
		body string
		want string
	}{
		{body: `{"name": ""}`, want: "Hello, World!"},
		{body: `{"name": "Gopher"}`, want: "Hello, Gopher!"},
	}

	for _, test := range tests {
		req := httptest.NewRequest("GET", "/", strings.NewReader(test.body))
		req.Header.Add("Content-Type", "application/json")

		rr := httptest.NewRecorder()
		HelloHTTP(rr, req)

		if got := rr.Body.String(); got != test.want {
			t.Errorf("HelloHTTP(%q) = %q, want %q", test.body, got, test.want)
		}
	}
}

Java


import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;

import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(JUnit4.class)
public class HelloHttpTest {
  @Mock private HttpRequest request;
  @Mock private HttpResponse response;

  private BufferedWriter writerOut;
  private StringWriter responseOut;
  private static final Gson gson = new Gson();

  @Before
  public void beforeTest() throws IOException {
    MockitoAnnotations.initMocks(this);

    // use an empty string as the default request content
    BufferedReader reader = new BufferedReader(new StringReader(""));
    when(request.getReader()).thenReturn(reader);

    responseOut = new StringWriter();
    writerOut = new BufferedWriter(responseOut);
    when(response.getWriter()).thenReturn(writerOut);
  }

  @Test
  public void helloHttp_noParamsGet() throws IOException {
    new HelloHttp().service(request, response);

    writerOut.flush();
    assertThat(responseOut.toString()).isEqualTo("Hello world!");
  }
}

C#

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace HelloHttp.Tests
{
    public class FunctionUnitTest
    {
        [Fact]
        public async Task GetRequest_NoParameters()
        {
            DefaultHttpContext context = new DefaultHttpContext();
            string text = await ExecuteRequest(context);
            Assert.Equal("Hello world!", text);
        }

        [Fact]
        public async Task GetRequest_UrlParameters()
        {
            DefaultHttpContext context = new DefaultHttpContext
            {
                Request = { QueryString = new QueryString("?name=Cho") }
            };
            string text = await ExecuteRequest(context);
            Assert.Equal("Hello Cho!", text);
        }

        [Fact]
        public async Task PostRequest_BodyParameters()
        {
            string json = "{\"name\":\"Julie\"}";
            DefaultHttpContext context = new DefaultHttpContext
            {
                Request =
                {
                    Method = HttpMethods.Post,
                    Body = new MemoryStream(Encoding.UTF8.GetBytes(json))
                }
            };
            string text = await ExecuteRequest(context);
            Assert.Equal("Hello Julie!", text);
        }

        /// <summary>
        /// Executes the given request in the function, validates that the response
        /// status code is 200, and returns the text of the response body.
        /// </summary>
        private static async Task<string> ExecuteRequest(HttpContext context)
        {
            MemoryStream responseStream = new MemoryStream();
            context.Response.Body = responseStream;

            Function function = new Function(new NullLogger<Function>());
            await function.HandleAsync(context);
            Assert.Equal(200, context.Response.StatusCode);
            context.Response.BodyWriter.Complete();
            context.Response.Body.Position = 0;

            TextReader reader = new StreamReader(responseStream);
            return reader.ReadToEnd();
        }
    }
}

Ruby

require "minitest/autorun"
require "functions_framework/testing"

describe "functions_helloworld_http" do
  include FunctionsFramework::Testing

  it "prints a name" do
    load_temporary "helloworld/http/app.rb" do
      request = make_post_request "http://example.com:8080/", '{"name": "Ruby"}'
      response = call_http "hello_http", request
      assert_equal 200, response.status
      assert_equal "Hello Ruby!", response.body.join
    end
  end

  it "prints hello world" do
    load_temporary "helloworld/http/app.rb" do
      request = make_post_request "http://example.com:8080/", ""
      response = call_http "hello_http", request
      assert_equal 200, response.status
      assert_equal "Hello World!", response.body.join
    end
  end
end

PHP


namespace Google\Cloud\Samples\Functions\HelloworldHttp;

use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;

/**
 * Class SampleUnitTest.
 *
 * Unit test for helloHttp.
 */
class SampleUnitTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        require_once __DIR__ . '/index.php';
    }

    public function testFunction(): void
    {
        $name = uniqid();
        $request = new ServerRequest('POST', '/', [], json_encode(['name' => $name]));
        $expected = sprintf('Hello, %s!', $name);
        $actual = helloHttp($request);
        $this->assertEquals($expected, $actual);
    }
}

この単体テストを実行するには、以下のコマンドを使用します。

Node.js

mocha test/sample.unit.http.test.js --exit

Python

pytest sample_http_test.py

Go

go test -v ./hello_http_test.go

Java

Maven は mvn test を使用して単体テストを実行します。

mvn test

C#

dotnet test

Ruby

bundle exec ruby functions/test/helloworld/http_test.rb

PHP

単体テストを実行するには、PHPUnit を使用します。

統合テスト

次のテストは、上記の関数の統合テストとして機能します。

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()

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!");
  }
}

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();
                }
            }
        }
    }
}

HTTP 関数の統合テストを実行するには、次のコマンドを使用します。上に示した統合テストサンプルは、以下に示す export コマンドで使用される値を変更することで構成できます。

Node.js

export PORT=8080
mocha test/sample.integration.http.test.js --exit

Python

export PORT=8080
pytest sample_http_test_integration.py

Java

Java 統合テストサンプルは常に localhost:8081 で実行され、環境変数は使用されませんMaven は、mvn verify を使用して統合テストを実行します。

mvn verify -Dit.test=ExampleIT

C#

dotnet test

システムテスト

次のテストは、上記の関数のシステムテストとして機能します。

Node.js

const assert = require('assert');
const Supertest = require('supertest');
const supertest = Supertest(process.env.BASE_URL);
const childProcess = require('child_process');

if (!process.env.GCF_REGION) {
  throw new Error('"GCF_REGION" env var must be set.');
}

describe('system tests', () => {
  it('helloHttp: should print a name', async () => {
    await supertest
      .post('/helloHttp')
      .send({name: 'John'})
      .expect(200)
      .expect(response => {
        assert.strictEqual(response.text, 'Hello John!');
      });
  });

Python

import os
import uuid

import requests

def test_no_args():
    BASE_URL = os.getenv('BASE_URL')
    assert BASE_URL is not None

    res = requests.get('{}/hello_http'.format(BASE_URL))
    assert res.text == 'Hello, World!'

def test_args():
    BASE_URL = os.getenv('BASE_URL')
    assert BASE_URL is not None

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

Go


package helloworld

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"
)

const RuntimeVersion = "go113"

func TestMain(m *testing.M) {
	// Only run end-to-end tests when configured to do so.
	if os.Getenv("GOLANG_SAMPLES_E2E_TEST") == "" {
		log.Println("Skipping end-to-end tests: GOLANG_SAMPLES_E2E_TEST not set")
		os.Exit(m.Run())
	}

	retn, err := setupAndRun(m)
	if err != nil {
		log.Fatal(err)
	}
	os.Exit(retn)
}

func setupAndRun(m *testing.M) (int, error) {
	entryPoint := "HelloHTTP"
	name := entryPoint + "-" + time.Now().Format("20060102-150405")

	// Setup function for tests.
	cmd := exec.Command("gcloud", "functions", "deploy", name,
		"--entry-point="+entryPoint,
		"--runtime="+RuntimeVersion,
		"--allow-unauthenticated",
		"--trigger-http",
	)
	log.Printf("Running: %s %s", cmd.Path, strings.Join(cmd.Args, " "))
	if _, err := cmd.Output(); err != nil {
		log.Println(string(err.(*exec.ExitError).Stderr))
		return 1, fmt.Errorf("Setup: Deploy function: %w", err)
	}

	// Tear down the deployed function.
	defer func() {
		cmd = exec.Command("gcloud", "functions", "delete", name)
		log.Printf("Running: %s %s", cmd.Path, strings.Join(cmd.Args, " "))
		if _, err := cmd.Output(); err != nil {
			log.Println(string(err.(*exec.ExitError).Stderr))
			log.Printf("Teardown: Delete function: %v", err)
		}
	}()

	// Retrieve URL for tests.
	cmd = exec.Command("gcloud", "functions", "describe", name, "--format=value(httpsTrigger.url)")
	log.Printf("Running: %s %s", cmd.Path, strings.Join(cmd.Args, " "))
	out, err := cmd.Output()
	if err != nil {
		log.Println(string(err.(*exec.ExitError).Stderr))
		return 1, fmt.Errorf("Setup: Get function URL: %w", err)
	}
	if err := os.Setenv("BASE_URL", strings.TrimSpace(string(out))); err != nil {
		return 1, fmt.Errorf("Setup: os.Setenv: %w", err)
	}

	// Run the tests.
	return m.Run(), nil
}

func TestHelloHTTPSystem(t *testing.T) {
	client := http.Client{
		Timeout: 10 * time.Second,
	}
	urlString := os.Getenv("BASE_URL")
	testURL, err := url.Parse(urlString)
	if err != nil {
		t.Fatalf("url.Parse(%q): %v", urlString, err)
	}

	tests := []struct {
		body string
		want string
	}{
		{body: `{"name": ""}`, want: "Hello, World!"},
		{body: `{"name": "Gopher"}`, want: "Hello, Gopher!"},
	}

	for _, test := range tests {
		req := &http.Request{
			Method: http.MethodPost,
			Body:   ioutil.NopCloser(strings.NewReader(test.body)),
			URL:    testURL,
		}
		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("HelloHTTP http.Get: %v", err)
		}
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatalf("HelloHTTP ioutil.ReadAll: %v", err)
		}
		if got := string(body); got != test.want {
			t.Errorf("HelloHTTP(%q) = %q, want %q", test.body, got, test.want)
		}
	}
}

Java


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

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ExampleSystemIT {
  // Root URL pointing to your Cloud Functions deployment
  // TODO<developer>: set this value, as an environment variable or within your test code
  private static final String BASE_URL = System.getenv("FUNCTIONS_BASE_URL");

  private static HttpClient client = HttpClient.newHttpClient();

  @Test
  public void helloHttp_shouldRunWithFunctionsFramework() throws IOException, InterruptedException {
    String functionUrl = BASE_URL + "/HelloHttp";

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

    HttpResponse response = client.send(getRequest, HttpResponse.BodyHandlers.ofString());
    assertThat(response.body().toString()).isEqualTo("Hello world!");
  }
}

HTTP 関数のシステムテストを実行するには、次のコマンドで関数をデプロイします。

Node.js

gcloud functions deploy helloHttp \
--runtime nodejs16 --trigger-http --allow-unauthenticated
優先する Node.js のバージョンを指定するには、--runtime フラグに次の値を使用します。
  • nodejs16(推奨)
  • nodejs14
  • nodejs12
  • nodejs10

Python

gcloud functions deploy hello_http \
--runtime python39 --trigger-http --allow-unauthenticated
優先する Python バージョンを指定するには、--runtime フラグに次の値を使用します。
  • python39(推奨)
  • python38
  • python37

Go

gcloud functions deploy HelloHTTP \
--runtime go116 --trigger-http --allow-unauthenticated
優先する Go バージョンを指定するには、--runtime フラグに次の値を使用します。
  • go116(推奨)
  • go113
  • go111

Java

gcloud functions deploy java-hello-http \
--entry-point functions.HelloHttp \
--runtime java11 \
--memory 512MB --trigger-http --allow-unauthenticated

デプロイされた HTTP 関数を、以下のコマンドを使用してテストします。HTTP Cloud Functions のシステムテストと統合テストとの主な違いは、関数へのアクセスを可能にする URL です。

Node.js

export BASE_URL=https://YOUR_GCF_REGION-YOUR_GCP_PROJECT_ID.cloudfunctions.net/
mocha test/sample.system.http.test.js --exit

Python

export BASE_URL=https://YOUR_GCF_REGION-YOUR_GCP_PROJECT_ID.cloudfunctions.net/
pytest sample_http_test_system.py

Go

export BASE_URL=https://YOUR_GCF_REGION-YOUR_GCP_PROJECT_ID.cloudfunctions.net/
go test -v ./hello_http_system_test.go

Java

Maven は、mvn verify を使用してシステムテストを実行します。

mvn verify -Dit.test=ExampleSystemIT

ここで

  • YOUR_GCF_REGION は Cloud Functions のリージョンです。
  • YOUR_GCP_PROJECT_ID は Cloud プロジェクト ID です。