컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

HTTP 함수 테스트

Cloud Functions에는 두 가지 유형이 있습니다. CloudEvent 함수와 백그라운드 함수는 내부 Cloud Platform 이벤트에 의해 트리거되는 이벤트 기반 함수이지만 HTTP 함수는 HTTP 요청에 의해 트리거됩니다.

함수의 테스트 구조는 함수가 사용하는 Google Cloud Platform 리소스에 따라 다릅니다. 한편 함수의 리소스 사용은 해당 함수가 트리거되는 방법에 따라 다릅니다.

이 문서에서는 HTTP Cloud Functions를 테스트하는 방법을 설명합니다. 이벤트 기반 함수를 테스트하는 방법은 이벤트 기반 함수 테스트를 참조하세요.

HTTP 트리거 함수

대부분의 Cloud Functions 단위 테스트와 마찬가지로 HTTP 트리거 함수 단위 테스트에도 특정 구조가 있습니다. 일반적으로 다른 함수 유형의 테스트는 구조가 유사하지 않지만 시스템 및 통합 테스트는 유사합니다.

다음은 HTTP 함수의 예시입니다.

Node.js

const functions = require('@google-cloud/functions-framework');
const escapeHtml = require('escape-html');

/**
 * Responds to an HTTP request using data from the request body parsed according
 * to the "content-type" header.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
functions.http('helloHttp', (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
});

Python

from flask import escape
import functions_framework

@functions_framework.http
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"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
	functions.HTTP("HelloHTTP", HelloHTTP)
}

// 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))
}

자바


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 Google\CloudFunctions\FunctionsFramework;
use Psr\Http\Message\ServerRequestInterface;

// Register the function with Functions Framework.
// This enables omitting the `FUNCTIONS_SIGNATURE_TYPE=http` environment
// variable when deploying. The `FUNCTION_TARGET` environment variable should
// match the first parameter.
FunctionsFramework::http('helloHttp', 'helloHttp');

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

ExpressSinon을 사용해 모의 처리됩니다.
const sinon = require('sinon');
const assert = require('assert');
require('../');

const getMocks = () => {
  const req = {body: {}, query: {}};

  return {
    req: req,
    res: {
      send: sinon.stub().returnsThis(),
    },
  };
};

it('helloHttp: should print a name', () => {
  const mocks = getMocks();

  const helloHttp = getFunction('helloHttp');
  helloHttp(mocks.req, mocks.res);

  assert.strictEqual(mocks.res.send.calledOnceWith('Hello World!'), true);
});

Python

Flaskunittest를 사용하여 모의 처리됩니다.
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)
		}
	}
}

자바


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

자바

Maven은 mvn test를 사용하여 단위 테스트를 실행합니다.

mvn test

C#

dotnet test

Ruby

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

PHP

단위 테스트를 실행하려면 PHPUnit를 사용합니다.

통합 테스트

다음 테스트는 위 함수의 통합 테스트로 사용됩니다.

Node.js

const supertest = require('supertest');

const {getTestServer} = require('@google-cloud/functions-framework/testing');
describe('functions_helloworld_http HTTP integration test', () => {
  it('helloHttp: should print a name with req body', async () => {
    const server = getTestServer('helloHttp');
    await supertest(server)
      .post('/')
      .send({name: 'John'})
      .set('Content-Type', 'application/json')
      .expect(200)
      .expect('Hello John!');
  });
});

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 = session.post(
      BASE_URL,
      json={'name': name}
    )
    assert res.text == 'Hello {}!'.format(name)

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

자바


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

자바

자바 통합 테스트 예시는 항상 localhost:8081에서 실행되며 환경 변수를 사용하지 않습니다. Mavenmvn verify를 사용하여 통합 테스트를 실행합니다.

mvn verify -Dit.test=ExampleIT

C#

dotnet test

시스템 테스트

다음 테스트는 위 함수의 시스템 테스트로 사용됩니다.

Node.js

const assert = require('assert');
const {request} = require('gaxios');
const childProcess = require('child_process');

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

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

const FUNCTION_URL = `${process.env.BASE_URL}/helloHttp`;

describe('system tests', () => {
  it('helloHttp: should print a name', async () => {
    const response = await request({
      url: FUNCTION_URL,
      method: 'POST',
      data: {name: 'John'},
    });
    assert.strictEqual(response.data, '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)
		}
	}
}

자바


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

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
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");

  // Identity token used to send requests to authenticated-only functions
  // TODO<developer>: Set this value if your function requires authentication.
  //                  See the documentation for more info:
  // https://cloud.google.com/functions/docs/securing/authenticating
  private static final String IDENTITY_TOKEN = System.getenv("FUNCTIONS_IDENTITY_TOKEN");

  // Name of the deployed function
  // TODO<developer>: Set this to HelloHttp, as an environment variable or within your test code
  private static final String FUNCTION_DEPLOYED_NAME = System.getenv("FUNCTIONS_HTTP_FN_NAME");

  private static HttpClient client = HttpClient.newHttpClient();

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

    HttpRequest.Builder getRequestBuilder = java.net.http.HttpRequest.newBuilder()
        .uri(URI.create(functionUrl))
        .GET();

    // Used to test functions that require authenticated invokers
    if (IDENTITY_TOKEN != null) {
      getRequestBuilder.header("Authorization", "Bearer " + IDENTITY_TOKEN);
    }

    java.net.http.HttpRequest getRequest = getRequestBuilder.build();

    HttpResponse response = client.send(getRequest, HttpResponse.BodyHandlers.ofString());

    assertThat(response.statusCode()).isEqualTo(HttpURLConnection.HTTP_OK);
    assertThat(response.body().toString()).isEqualTo("Hello world!");
  }
}

HTTP 함수에 대한 시스템 테스트를 실행하려면 다음 명령어를 사용하여 함수를 배포합니다.

Node.js

gcloud functions deploy helloHttp \
--runtime nodejs18 --trigger-http --allow-unauthenticated
--runtime 플래그에 다음 값을 사용하여 원하는 Node.js 버전을 지정할 수 있습니다.
  • nodejs18(권장)
  • nodejs16
  • nodejs14
  • nodejs12
  • nodejs10

Python

gcloud functions deploy hello_http \
--runtime python311 --trigger-http --allow-unauthenticated
--runtime 플래그에 다음 값을 사용하여 선호하는 Python 버전을 지정할 수 있습니다.
  • python311(권장)
  • python310
  • python39
  • python38
  • python37

Go

gcloud functions deploy HelloHTTP \
--runtime go119 --trigger-http --allow-unauthenticated
--runtime 플래그에 다음 값을 사용하여 원하는 Go 버전을 지정할 수 있습니다.
  • go119(권장)
  • go120(미리보기)
  • go118
  • go116
  • go113

자바

gcloud functions deploy java-hello-http \
--entry-point functions.HelloHttp \
--runtime java17 \
--memory 512MB --trigger-http --allow-unauthenticated
--runtime 플래그에 다음 값을 사용하여 원하는 자바 버전을 지정할 수 있습니다.
  • java17(권장)
  • java11

배포된 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

자바

Maven은 mvn verify를 사용하여 시스템 테스트를 실행합니다.

mvn verify -Dit.test=ExampleSystemIT

각 항목의 의미는 다음과 같습니다.

  • YOUR_GCF_REGION은 Cloud Functions 리전입니다.
  • YOUR_GCP_PROJECT_ID는 Cloud 프로젝트 ID입니다.