Existen estos dos tipos de funciones de Cloud Functions: funciones de HTTP y funciones controladas por eventos. Cada tipo tiene sus propios requisitos de prueba.
La estructura de prueba de una función depende de los recursos de Google Cloud que use esa función. A su vez, el uso de los recursos de una función depende de cómo se active esa función.
En este documento, se describe cómo probar funciones de Cloud Functions controladas por eventos. Consulta Prueba las funciones de HTTP para obtener información sobre cómo evaluar las funciones de HTTP.
Funciones activadas por Pub/Sub
Las pruebas de funciones activadas por Pub/Sub se organizan de manera diferente según dónde se ejecute la función probada.
A continuación, se muestra un ejemplo de una función activada por Pub/Sub que imprime “Hello, World”:
Node.js
/**
* Background Cloud Function to be triggered by Pub/Sub.
* This function is exported by index.js, and executed when
* the trigger topic receives a message.
*
* @param {object} message The Pub/Sub message.
* @param {object} context The event metadata.
*/
exports.helloPubSub = (message, context) => {
const name = message.data
? Buffer.from(message.data, 'base64').toString()
: 'World';
console.log(`Hello, ${name}!`);
};
Python
def hello_pubsub(event, context):
"""Background Cloud Function to be triggered by Pub/Sub.
Args:
event (dict): The dictionary with data specific to this type of
event. The `data` field contains the PubsubMessage message. The
`attributes` field will contain custom attributes if there are any.
context (google.cloud.functions.Context): The Cloud Functions event
metadata. The `event_id` field contains the Pub/Sub message ID. The
`timestamp` field contains the publish time.
"""
import base64
print("""This Function was triggered by messageId {} published at {}
""".format(context.event_id, context.timestamp))
if 'data' in event:
name = base64.b64decode(event['data']).decode('utf-8')
else:
name = 'World'
print('Hello {}!'.format(name))
Go
// Package helloworld provides a set of Cloud Functions samples.
package helloworld
import (
"context"
"log"
)
// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
Data []byte `json:"data"`
}
// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
name := string(m.Data) // Automatically decoded from base64.
if name == "" {
name = "World"
}
log.Printf("Hello, %s!", name)
return nil
}
Java
import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.events.cloud.pubsub.v1.Message;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;
public class HelloPubSub implements BackgroundFunction<Message> {
private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());
@Override
public void accept(Message message, Context context) {
String name = "world";
if (message != null && message.getData() != null) {
name = new String(
Base64.getDecoder().decode(message.getData().getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8);
}
logger.info(String.format("Hello %s!", name));
return;
}
}
C#
using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace HelloPubSub
{
public class Function : ICloudEventFunction<MessagePublishedData>
{
private readonly ILogger _logger;
public Function(ILogger<Function> logger) =>
_logger = logger;
public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken)
{
string nameFromMessage = data.Message?.TextData;
string name = string.IsNullOrEmpty(nameFromMessage) ? "world" : nameFromMessage;
_logger.LogInformation("Hello {name}", name);
return Task.CompletedTask;
}
}
}
Pruebas de unidades
Estas son las pruebas de unidades para la función activada por Pub/Sub mencionada anteriormente:
Node.js
const assert = require('assert');
const uuid = require('uuid');
const sinon = require('sinon');
const {helloPubSub} = require('..');
const stubConsole = function () {
sinon.stub(console, 'error');
sinon.stub(console, 'log');
};
const restoreConsole = function () {
console.log.restore();
console.error.restore();
};
beforeEach(stubConsole);
afterEach(restoreConsole);
it('helloPubSub: should print a name', () => {
// Create mock Pub/Sub event
const name = uuid.v4();
const event = {
data: Buffer.from(name).toString('base64'),
};
// Call tested function and verify its behavior
helloPubSub(event);
assert.ok(console.log.calledWith(`Hello, ${name}!`));
});
Python
import base64
import mock
import main
mock_context = mock.Mock()
mock_context.event_id = '617187464135194'
mock_context.timestamp = '2019-07-15T22:09:03.761Z'
def test_print_hello_world(capsys):
data = {}
# Call tested function
main.hello_pubsub(data, mock_context)
out, err = capsys.readouterr()
assert 'Hello World!' in out
def test_print_name(capsys):
name = 'test'
data = {'data': base64.b64encode(name.encode())}
# Call tested function
main.hello_pubsub(data, mock_context)
out, err = capsys.readouterr()
assert 'Hello {}!\n'.format(name) in out
Go
package helloworld
import (
"context"
"io/ioutil"
"log"
"os"
"testing"
)
func TestHelloPubSub(t *testing.T) {
tests := []struct {
data string
want string
}{
{want: "Hello, World!\n"},
{data: "Go", want: "Hello, Go!\n"},
}
for _, test := range tests {
r, w, _ := os.Pipe()
log.SetOutput(w)
originalFlags := log.Flags()
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
m := PubSubMessage{
Data: []byte(test.data),
}
HelloPubSub(context.Background(), m)
w.Close()
log.SetOutput(os.Stderr)
log.SetFlags(originalFlags)
out, err := ioutil.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if got := string(out); got != test.want {
t.Errorf("HelloPubSub(%q) = %q, want %q", test.data, got, test.want)
}
}
}
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.common.testing.TestLogHandler;
import com.google.events.cloud.pubsub.v1.Message;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for main.java.com.example.functions.helloworld.HelloPubSub.
*/
@RunWith(JUnit4.class)
public class HelloPubSubTest {
private HelloPubSub sampleUnderTest;
private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());
private static final TestLogHandler LOG_HANDLER = new TestLogHandler();
@Before
public void setUp() {
sampleUnderTest = new HelloPubSub();
logger.addHandler(LOG_HANDLER);
LOG_HANDLER.clear();
}
@Test
public void helloPubSub_shouldPrintName() {
Message message = new Message();
message.setData(Base64.getEncoder().encodeToString(
"John".getBytes(StandardCharsets.UTF_8)));
sampleUnderTest.accept(message, null);
String logMessage = LOG_HANDLER.getStoredLogRecords().get(0).getMessage();
assertThat("Hello John!").isEqualTo(logMessage);
}
@Test
public void helloPubSub_shouldPrintHelloWorld() {
Message message = new Message();
sampleUnderTest.accept(message, null);
String logMessage = LOG_HANDLER.getStoredLogRecords().get(0).getMessage();
assertThat("Hello world!").isEqualTo(logMessage);
}
}
C#
using CloudNative.CloudEvents;
using Google.Cloud.Functions.Testing;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace HelloWorld.Tests
{
public class HelloPubSubUnitTest
{
[Fact]
public async Task MessageWithTextData()
{
var data = new MessagePublishedData { Message = new PubsubMessage { TextData = "PubSub user" } };
var cloudEvent = new CloudEvent(
MessagePublishedData.MessagePublishedCloudEventType,
new Uri("//pubsub.googleapis.com", UriKind.RelativeOrAbsolute),
Guid.NewGuid().ToString(),
DateTime.UtcNow);
// Our function doesn't actually use the CloudEvent data, because that's provided separately.
// If we wanted to make the CloudEvent look as realistic as possible, we could use
// CloudEventConverters.PopulateCloudEvent.
var logger = new MemoryLogger<HelloPubSub.Function>();
var function = new HelloPubSub.Function(logger);
await function.HandleAsync(cloudEvent, data, CancellationToken.None);
var logEntry = Assert.Single(logger.ListLogEntries());
Assert.Equal("Hello PubSub user", logEntry.Message);
Assert.Equal(LogLevel.Information, logEntry.Level);
}
[Fact]
public async Task MessageWithoutTextData()
{
var data = new MessagePublishedData
{
Message = new PubsubMessage { Attributes = { { "key", "value" } } }
};
var cloudEvent = new CloudEvent(
MessagePublishedData.MessagePublishedCloudEventType,
new Uri("//pubsub.googleapis.com", UriKind.RelativeOrAbsolute),
Guid.NewGuid().ToString(),
DateTime.UtcNow);
var logger = new MemoryLogger<HelloPubSub.Function>();
var function = new HelloPubSub.Function(logger);
await function.HandleAsync(cloudEvent, data, CancellationToken.None);
var logEntry = Assert.Single(logger.ListLogEntries());
Assert.Equal("Hello world", logEntry.Message);
Assert.Equal(LogLevel.Information, logEntry.Level);
}
}
}
Ejecuta las pruebas de unidades con el siguiente comando:
Node.js
mocha test/sample.unit.pubsub.test.js --exit
Python
pytest sample_pubsub_test.py
Go
go test -v ./hello_pubsub_test.go
Java
Maven usamvn test
para ejecutar pruebas de unidades.
mvn test
C#
dotnet test
Pruebas de integración
Estas son las pruebas de integración para la función activada por Pub/Sub mencionada anteriormente:
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 cwd = path.join(__dirname, '..');
it('helloPubSub: should print a name', async () => {
const name = uuid.v4();
const PORT = 8088; // Each running framework instance needs a unique port
const encodedName = Buffer.from(name).toString('base64');
const pubsubMessage = {data: {data: encodedName}};
// exec's 'timeout' param won't kill children of "shim" /bin/sh process
// Workaround: include "& sleep <TIMEOUT>; kill $!" in executed command
const proc = execPromise(
`functions-framework --target=helloPubSub --signature-type=event --port=${PORT} & sleep 1; kill $!`,
{shell: true, cwd}
);
// Send HTTP request simulating Pub/Sub message
// (GCF translates Pub/Sub messages to HTTP requests internally)
const response = await requestRetry({
url: `http://localhost:${PORT}/`,
method: 'POST',
body: pubsubMessage,
retryDelay: 200,
json: true,
});
assert.strictEqual(response.statusCode, 204);
// Wait for the functions framework to stop
const {stdout} = await proc;
assert(stdout.includes(`Hello, ${name}!`));
});
});
Python
import base64
import os
import subprocess
import uuid
import requests
from requests.packages.urllib3.util.retry import Retry
def test_print_name():
name = str(uuid.uuid4())
port = 8088 # Each running framework instance needs a unique port
encoded_name = base64.b64encode(name.encode('utf-8')).decode('utf-8')
pubsub_message = {
'data': {'data': encoded_name}
}
process = subprocess.Popen(
[
'functions-framework',
'--target', 'hello_pubsub',
'--signature-type', 'event',
'--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)
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(url, retry_adapter)
response = session.post(url, json=pubsub_message)
assert response.status_code == 200
# Stop the functions framework process
process.kill()
process.wait()
out, err = process.communicate()
print(out, err, response.content)
assert f'Hello {name}!' in str(out)
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.gson.Gson;
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 io.vavr.CheckedRunnable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
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 = HttpClientBuilder.create().build();
private static final Gson gson = new Gson();
@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() {
// Terminate the running Functions Framework Maven plugin process (if it's still running)
if (emulatorProcess.isAlive()) {
emulatorProcess.destroy();
}
}
@Test
public void helloPubSub_shouldRunWithFunctionsFramework() throws Throwable {
String functionUrl = BASE_URL + "/helloPubsub"; // URL to your locally-running function
// Initialize constants
String name = UUID.randomUUID().toString();
String nameBase64 = Base64.getEncoder().encodeToString(name.getBytes(StandardCharsets.UTF_8));
String jsonStr = gson.toJson(Map.of("data", Map.of("data", nameBase64)));
HttpPost postRequest = new HttpPost(URI.create(functionUrl));
postRequest.setEntity(new StringEntity(jsonStr));
// 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)
.retryExceptions(HttpHostConnectException.class)
.intervalFunction(IntervalFunction.ofExponentialBackoff(200, 2))
.build());
Retry retry = registry.retry("my");
// Perform the request-retry process
CheckedRunnable retriableFunc = Retry.decorateCheckedRunnable(
retry, () -> client.execute(postRequest));
retriableFunc.run();
// Get Functions Framework plugin process' stdout
InputStream stdoutStream = emulatorProcess.getErrorStream();
ByteArrayOutputStream stdoutBytes = new ByteArrayOutputStream();
stdoutBytes.write(stdoutStream.readNBytes(stdoutStream.available()));
// Verify desired name value is present
assertThat(stdoutBytes.toString(StandardCharsets.UTF_8)).contains(
String.format("Hello %s!", name));
}
}
Puedes ejecutar las pruebas de integración para esta función de la siguiente manera:
Node.js
mocha test/sample.integration.pubsub.test.js --exit
Python
pytest sample_pubsub_test_integration.py
Java
Maven usamvn verify
para ejecutar pruebas de integración.
mvn verify -Dit.test=ExampleIT
Pruebas de sistema
Estas son las pruebas de sistema para esta función:
Node.js
const childProcess = require('child_process');
const assert = require('assert');
const uuid = require('uuid');
const {PubSub} = require('@google-cloud/pubsub');
const moment = require('moment');
const promiseRetry = require('promise-retry');
const pubsub = new PubSub();
const topicName = process.env.FUNCTIONS_TOPIC;
const baseCmd = 'gcloud functions';
describe('system tests', () => {
it('helloPubSub: should print a name', async () => {
const name = uuid.v4();
// Subtract time to work-around local-GCF clock difference
const startTime = moment().subtract(4, 'minutes').toISOString();
// Publish to pub/sub topic
const topic = pubsub.topic(topicName);
await topic.publish(Buffer.from(name));
console.log(`published topic ${topicName}, ${name}`);
// Wait for logs to become consistent
await promiseRetry(retry => {
const logs = childProcess
.execSync(`${baseCmd} logs read helloPubSub --start-time ${startTime}`)
.toString();
try {
assert.ok(logs.includes(`Hello, ${name}!`));
} catch (err) {
retry(err);
}
});
});
Python
from datetime import datetime
from os import getenv
import subprocess
import time
import uuid
from google.cloud import pubsub_v1
import pytest
PROJECT = getenv('GCP_PROJECT')
TOPIC = getenv('FUNCTIONS_TOPIC')
assert PROJECT is not None
assert TOPIC is not None
@pytest.fixture(scope='module')
def publisher_client():
yield pubsub_v1.PublisherClient()
def test_print_name(publisher_client):
start_time = datetime.utcnow().isoformat()
topic_path = publisher_client.topic_path(PROJECT, TOPIC)
# Publish the message
name = uuid.uuid4()
data = str(name).encode('utf-8')
publisher_client.publish(topic_path, data=data).result()
# Wait for logs to become consistent
time.sleep(15)
# Check logs after a delay
log_process = subprocess.Popen([
'gcloud',
'alpha',
'functions',
'logs',
'read',
'hello_pubsub',
'--start-time',
start_time
], stdout=subprocess.PIPE)
logs = str(log_process.communicate()[0])
assert 'Hello {}!'.format(name) in logs
Go
package helloworld
import (
"context"
"log"
"os"
"os/exec"
"strings"
"testing"
"time"
"cloud.google.com/go/pubsub"
"github.com/gobuffalo/uuid"
)
func TestHelloPubSubSystem(t *testing.T) {
ctx := context.Background()
topicName := os.Getenv("FUNCTIONS_TOPIC")
projectID := os.Getenv("GCP_PROJECT")
startTime := time.Now().UTC().Format(time.RFC3339)
// Create the Pub/Sub client and topic.
client, err := pubsub.NewClient(ctx, projectID)
if err != nil {
log.Fatal(err)
}
topic := client.Topic(topicName)
// Publish a message with a random string to verify.
// We use a random string to make sure the function is logging the correct
// message for this test invocation.
u := uuid.Must(uuid.NewV4())
msg := &pubsub.Message{
Data: []byte(u.String()),
}
topic.Publish(ctx, msg).Get(ctx)
// Wait for logs to be consistent.
time.Sleep(20 * time.Second)
// Check logs after a delay.
cmd := exec.Command("gcloud", "alpha", "functions", "logs", "read", "HelloPubSub", "--start-time", startTime)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("exec.Command: %v", err)
}
if got := string(out); !strings.Contains(got, u.String()) {
t.Errorf("HelloPubSub got %q, want to contain %q", got, u.String())
}
}
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.api.gax.paging.Page;
import com.google.cloud.logging.LogEntry;
import com.google.cloud.logging.Logging;
import com.google.cloud.logging.LoggingOptions;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.ProjectTopicName;
import com.google.pubsub.v1.PubsubMessage;
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.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.junit.BeforeClass;
import org.junit.Test;
public class ExampleSystemIT {
// TODO<developer>: set these values (as environment variables)
private static final String PROJECT_ID = System.getenv("GCP_PROJECT");
private static final String TOPIC_NAME = System.getenv("FUNCTIONS_SYSTEM_TEST_TOPIC");
private static final String FUNCTION_DEPLOYED_NAME = "HelloPubSub";
private static Logging loggingClient;
private static Publisher publisher;
private HelloPubSub sampleUnderTest;
@BeforeClass
public static void setUp() throws IOException {
loggingClient = LoggingOptions.getDefaultInstance().getService();
publisher = Publisher.newBuilder(
ProjectTopicName.of(PROJECT_ID, TOPIC_NAME)).build();
}
private static String getLogEntriesAsString(String startTimestamp) {
// Construct Stackdriver logging filter
// See this page for more info: https://cloud.google.com/logging/docs/view/advanced-queries
String filter = "resource.type=\"cloud_function\""
+ " AND severity=INFO"
+ " AND resource.labels.function_name=" + FUNCTION_DEPLOYED_NAME
+ String.format(" AND timestamp>=\"%s\"", startTimestamp);
// Get Stackdriver logging entries
Page<LogEntry> logEntries =
loggingClient.listLogEntries(
Logging.EntryListOption.filter(filter),
Logging.EntryListOption.sortOrder(
Logging.SortingField.TIMESTAMP, Logging.SortingOrder.DESCENDING)
);
// Serialize Stackdriver logging entries + collect them into a single string
String logsConcat = StreamSupport.stream(logEntries.getValues().spliterator(), false)
.map((x) -> x.toString())
.collect(Collectors.joining("%n"));
return logsConcat;
}
@Test
public void helloPubSub_shouldRunOnGcf() throws Exception {
String name = UUID.randomUUID().toString();
// Subtract time to work-around local-GCF clock difference
Instant startInstant = Instant.now().minus(Duration.ofMinutes(4));
String startTimestamp = DateTimeFormatter.ISO_INSTANT.format(startInstant);
// Publish to pub/sub topic
ByteString byteStr = ByteString.copyFrom(name, StandardCharsets.UTF_8);
PubsubMessage pubsubApiMessage = PubsubMessage.newBuilder().setData(byteStr).build();
publisher.publish(pubsubApiMessage).get();
// Keep retrying until the logs contain the desired invocation's log entry
// (If the invocation failed, the retry process will eventually time out)
RetryRegistry registry = RetryRegistry.of(RetryConfig.custom()
.maxAttempts(8)
.intervalFunction(IntervalFunction.ofExponentialBackoff(1000, 2))
.retryOnResult(s -> !s.toString().contains(name))
.build());
Retry retry = registry.retry(name);
String logEntry = Retry
.decorateFunction(retry, ExampleSystemIT::getLogEntriesAsString)
.apply(startTimestamp);
// Perform final assertion (to make sure we fail on timeout)
assertThat(logEntry).contains(name);
}
}
Para ejecutar las pruebas de sistema, sigue estas instrucciones:
En el proyecto de Cloud, selecciona un tema de Pub/Sub al que te suscribirás. Si proporcionas el nombre de un tema de Pub/Sub que no existe, se crea de forma automática.
Luego implementa las funciones con el siguiente comando:
Node.js
gcloud functions deploy helloPubSub \ --runtime nodejs10 \
Puedes usar los siguientes valores para que la marca
--trigger-topic YOUR_PUBSUB_TOPIC--runtime
especifique tu versión preferida de Node.js:nodejs10
nodejs12
nodejs14
(vista previa pública)
Python
gcloud functions deploy hello_pubsub \ --runtime python38 \
Puedes usar los siguientes valores para que la marca
--trigger-topic YOUR_PUBSUB_TOPIC--runtime
especifique tu versión preferida de Python:python37
python38
python39
(vista previa pública)
Comienza a usarlo
gcloud functions deploy HelloPubSub \ --runtime go113 \
Puedes usar los siguientes valores en la marca
--trigger-topic YOUR_PUBSUB_TOPIC--runtime
para especificar tu versión preferida de Go:go111
(obsoleta)go113
Java
gcloud functions deploy java-hello-pubsub \ --entry-point functions.HelloPubSub \ --runtime java11 \ --memory 512MB \
--trigger-topic YOUR_PUBSUB_TOPICYOUR_PUBSUB_TOPIC
es el nombre del tema de Pub/Sub al que quieres suscribir las funciones.Ejecuta las pruebas del sistema con el siguiente comando:
Node.js
export FUNCTIONS_TOPIC=YOUR_PUBSUB_TOPIC mocha test/sample.system.pubsub.test.js --exit
Python
export FUNCTIONS_TOPIC=YOUR_PUBSUB_TOPIC pytest sample_pubsub_test_system.py
Go
export FUNCTIONS_TOPIC=YOUR_PUBSUB_TOPIC go test -v ./hello_pubsub_system_test.go
Java
export FUNCTIONS_TOPIC=YOUR_PUBSUB_TOPIC mvn verify -Dit.test=ExampleSystemIT
YOUR_PUBSUB_TOPIC
es el nombre del tema de Pub/Sub al que quieres suscribir las funciones.
Funciones activadas por Storage
Las pruebas para las funciones activadas por Storage poseen una estructura similar a la de sus contrapartes activadas por Pub/Sub. Al igual que las pruebas de funciones activadas por Pub/Sub, estas pruebas activadas por Storage se organizan de forma diferente según la ubicación en la que se aloja la función que se prueba.
Aquí te mostramos un ejemplo de una función activada por Storage:
Node.js
/**
* Generic background Cloud Function to be triggered by Cloud Storage.
*
* @param {object} file The Cloud Storage file metadata.
* @param {object} context The event metadata.
*/
exports.helloGCS = (file, context) => {
console.log(` Event: ${context.eventId}`);
console.log(` Event Type: ${context.eventType}`);
console.log(` Bucket: ${file.bucket}`);
console.log(` File: ${file.name}`);
console.log(` Metageneration: ${file.metageneration}`);
console.log(` Created: ${file.timeCreated}`);
console.log(` Updated: ${file.updated}`);
};
Python
def hello_gcs(event, context):
"""Background Cloud Function to be triggered by Cloud Storage.
This generic function logs relevant data when a file is changed.
Args:
event (dict): The dictionary with data specific to this type of event.
The `data` field contains a description of the event in
the Cloud Storage `object` format described here:
https://cloud.google.com/storage/docs/json_api/v1/objects#resource
context (google.cloud.functions.Context): Metadata of triggering event.
Returns:
None; the output is written to Stackdriver Logging
"""
print('Event ID: {}'.format(context.event_id))
print('Event type: {}'.format(context.event_type))
print('Bucket: {}'.format(event['bucket']))
print('File: {}'.format(event['name']))
print('Metageneration: {}'.format(event['metageneration']))
print('Created: {}'.format(event['timeCreated']))
print('Updated: {}'.format(event['updated']))
Go
// Package helloworld provides a set of Cloud Functions samples.
package helloworld
import (
"context"
"fmt"
"log"
"time"
"cloud.google.com/go/functions/metadata"
)
// GCSEvent is the payload of a GCS event.
type GCSEvent struct {
Kind string `json:"kind"`
ID string `json:"id"`
SelfLink string `json:"selfLink"`
Name string `json:"name"`
Bucket string `json:"bucket"`
Generation string `json:"generation"`
Metageneration string `json:"metageneration"`
ContentType string `json:"contentType"`
TimeCreated time.Time `json:"timeCreated"`
Updated time.Time `json:"updated"`
TemporaryHold bool `json:"temporaryHold"`
EventBasedHold bool `json:"eventBasedHold"`
RetentionExpirationTime time.Time `json:"retentionExpirationTime"`
StorageClass string `json:"storageClass"`
TimeStorageClassUpdated time.Time `json:"timeStorageClassUpdated"`
Size string `json:"size"`
MD5Hash string `json:"md5Hash"`
MediaLink string `json:"mediaLink"`
ContentEncoding string `json:"contentEncoding"`
ContentDisposition string `json:"contentDisposition"`
CacheControl string `json:"cacheControl"`
Metadata map[string]interface{} `json:"metadata"`
CRC32C string `json:"crc32c"`
ComponentCount int `json:"componentCount"`
Etag string `json:"etag"`
CustomerEncryption struct {
EncryptionAlgorithm string `json:"encryptionAlgorithm"`
KeySha256 string `json:"keySha256"`
}
KMSKeyName string `json:"kmsKeyName"`
ResourceState string `json:"resourceState"`
}
// HelloGCS consumes a GCS event.
func HelloGCS(ctx context.Context, e GCSEvent) error {
meta, err := metadata.FromContext(ctx)
if err != nil {
return fmt.Errorf("metadata.FromContext: %v", err)
}
log.Printf("Event ID: %v\n", meta.EventID)
log.Printf("Event type: %v\n", meta.EventType)
log.Printf("Bucket: %v\n", e.Bucket)
log.Printf("File: %v\n", e.Name)
log.Printf("Metageneration: %v\n", e.Metageneration)
log.Printf("Created: %v\n", e.TimeCreated)
log.Printf("Updated: %v\n", e.Updated)
return nil
}
Java
import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import functions.eventpojos.GcsEvent;
import java.util.logging.Logger;
public class HelloGcs implements BackgroundFunction<GcsEvent> {
private static final Logger logger = Logger.getLogger(HelloGcs.class.getName());
@Override
public void accept(GcsEvent event, Context context) {
logger.info("Event: " + context.eventId());
logger.info("Event Type: " + context.eventType());
logger.info("Bucket: " + event.getBucket());
logger.info("File: " + event.getName());
logger.info("Metageneration: " + event.getMetageneration());
logger.info("Created: " + event.getTimeCreated());
logger.info("Updated: " + event.getUpdated());
}
}
C#
using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.Storage.V1;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace HelloGcs
{
public class Function : ICloudEventFunction<StorageObjectData>
{
private readonly ILogger _logger;
public Function(ILogger<Function> logger) =>
_logger = logger;
public Task HandleAsync(CloudEvent cloudEvent, StorageObjectData data, CancellationToken cancellationToken)
{
_logger.LogInformation("Event: {event}", cloudEvent.Id);
_logger.LogInformation("Event Type: {type}", cloudEvent.Type);
_logger.LogInformation("Bucket: {bucket}", data.Bucket);
_logger.LogInformation("File: {file}", data.Name);
_logger.LogInformation("Metageneration: {metageneration}", data.Metageneration);
_logger.LogInformation("Created: {created:s}", data.TimeCreated?.ToDateTimeOffset());
_logger.LogInformation("Updated: {updated:s}", data.Updated?.ToDateTimeOffset());
return Task.CompletedTask;
}
}
}
Pruebas de unidades
Estas son las pruebas de unidades para la función activada por Storage mencionada anteriormente:
Node.js
const assert = require('assert');
const uuid = require('uuid');
const sinon = require('sinon');
const {helloGCS} = require('..');
const stubConsole = function () {
sinon.stub(console, 'error');
sinon.stub(console, 'log');
};
const restoreConsole = function () {
console.log.restore();
console.error.restore();
};
beforeEach(stubConsole);
afterEach(restoreConsole);
it('helloGCS: should print out event', () => {
// Initialize mocks
const filename = uuid.v4();
const eventType = 'google.storage.object.finalize';
const event = {
name: filename,
resourceState: 'exists',
metageneration: '1',
};
const context = {
eventId: 'g1bb3r1sh',
eventType: eventType,
};
// Call tested function and verify its behavior
helloGCS(event, context);
assert.ok(console.log.calledWith(` File: ${filename}`));
assert.ok(console.log.calledWith(` Event Type: ${eventType}`));
});
Python
import mock
import main
def test_print(capsys):
name = 'test'
event = {
'bucket': 'some-bucket',
'name': name,
'metageneration': 'some-metageneration',
'timeCreated': '0',
'updated': '0'
}
context = mock.MagicMock()
context.event_id = 'some-id'
context.event_type = 'gcs-event'
# Call tested function
main.hello_gcs(event, context)
out, err = capsys.readouterr()
assert 'File: {}\n'.format(name) in out
Go
package helloworld
import (
"context"
"io/ioutil"
"log"
"os"
"strings"
"testing"
"cloud.google.com/go/functions/metadata"
)
func TestHelloGCS(t *testing.T) {
r, w, _ := os.Pipe()
log.SetOutput(w)
originalFlags := log.Flags()
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
name := "hello_gcs.txt"
e := GCSEvent{
Name: name,
}
meta := &metadata.Metadata{
EventID: "event ID",
}
ctx := metadata.NewContext(context.Background(), meta)
HelloGCS(ctx, e)
w.Close()
log.SetOutput(os.Stderr)
log.SetFlags(originalFlags)
out, err := ioutil.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
got := string(out)
wants := []string{
"File: " + name,
"Event ID: " + meta.EventID,
}
for _, want := range wants {
if !strings.Contains(got, want) {
t.Errorf("HelloGCS(%v) = %q, want to contain %q", e, got, want)
}
}
}
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.common.testing.TestLogHandler;
import functions.eventpojos.GcsEvent;
import functions.eventpojos.MockContext;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Unit tests for main.java.com.example.functions.helloworld.HelloGcs.
*/
public class HelloGcsTest {
private static final TestLogHandler LOG_HANDLER = new TestLogHandler();
private static final Logger logger = Logger.getLogger(HelloGcs.class.getName());
@Before
public void beforeTest() throws Exception {
logger.addHandler(LOG_HANDLER);
}
@After
public void afterTest() {
LOG_HANDLER.clear();
}
@Test
public void helloGcs_shouldPrintFileName() {
GcsEvent event = new GcsEvent();
event.setName("foo.txt");
MockContext context = new MockContext();
context.eventType = "google.storage.object.finalize";
new HelloGcs().accept(event, context);
String message = LOG_HANDLER.getStoredLogRecords().get(3).getMessage();
assertThat(message).contains("File: foo.txt");
}
}
C#
using CloudNative.CloudEvents;
using Google.Cloud.Functions.Testing;
using Google.Events;
using Google.Events.Protobuf.Cloud.Storage.V1;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace HelloWorld.Tests
{
public class HelloGcsUnitTest
{
[Fact]
public async Task FileNameIsLogged()
{
// Prepare the inputs
var cloudEvent = new CloudEvent(StorageObjectData.FinalizedCloudEventType, new Uri("//storage.googleapis.com"));
var data = new StorageObjectData { Name = "new-file.txt" };
CloudEventConverters.PopulateCloudEvent(cloudEvent, data);
var logger = new MemoryLogger<HelloGcs.Function>();
// Execute the function
var function = new HelloGcs.Function(logger);
await function.HandleAsync(cloudEvent, data, CancellationToken.None);
// Check the log results - just the entry starting with "File:".
var logEntry = Assert.Single(logger.ListLogEntries(), entry => entry.Message.StartsWith("File:"));
Assert.Equal("File: new-file.txt", logEntry.Message);
Assert.Equal(LogLevel.Information, logEntry.Level);
}
}
}
Ejecuta las pruebas de unidades con el siguiente comando:
Node.js
mocha test/sample.unit.storage.test.js --exit
Python
pytest sample_storage_test.py
Go
go test -v ./hello_cloud_storage_test.go
Java
Maven usamvn test
para ejecutar pruebas de unidades.
mvn test
C#
dotnet test
Pruebas de integración
Estas son las pruebas de integración para la función activada por Storage mencionada anteriormente:
Node.js
const assert = require('assert');
const execPromise = require('child-process-promise').exec;
const path = require('path');
const uuid = require('uuid');
const requestRetry = require('requestretry');
const cwd = path.join(__dirname, '..');
it('helloGCSGeneric: should print GCS event', async () => {
const filename = uuid.v4(); // Use a unique filename to avoid conflicts
const PORT = 9000; // Each running framework instance needs a unique port
const eventType = 'google.storage.object.finalize';
const data = {
data: {
name: filename,
resourceState: 'exists',
metageneration: '1',
},
context: {
eventType: eventType,
},
};
// Run the functions-framework instance to host functions locally
// exec's 'timeout' param won't kill children of "shim" /bin/sh process
// Workaround: include "& sleep <TIMEOUT>; kill $!" in executed command
const proc = execPromise(
`functions-framework --target=helloGCS --signature-type=event --port=${PORT} & sleep 1; kill $!`,
{shell: true, cwd}
);
// Send HTTP request simulating GCS change notification
// (GCF translates GCS notifications to HTTP requests internally)
const response = await requestRetry({
url: `http://localhost:${PORT}/`,
method: 'POST',
body: data,
retryDelay: 200,
json: true,
});
assert.strictEqual(response.statusCode, 204);
// Wait for functions-framework process to exit
const {stdout} = await proc;
assert.ok(stdout.includes(`File: ${filename}`));
assert.ok(stdout.includes(`Event Type: ${eventType}`));
});
});
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.gson.Gson;
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 io.vavr.CheckedRunnable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
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 final HttpClient client = HttpClientBuilder.create().build();
private static final Gson gson = new Gson();
@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() {
// Terminate the running Functions Framework Maven plugin process (if it's still running)
if (emulatorProcess.isAlive()) {
emulatorProcess.destroy();
}
}
@Test
public void helloGcs_shouldRunWithFunctionsFramework() throws Throwable {
String functionUrl = BASE_URL + "/helloGcs"; // URL to your locally-running function
// Initialize constants
String name = UUID.randomUUID().toString();
String jsonStr = gson.toJson(Map.of(
"data", Map.of(
"name", name, "resourceState", "exists", "metageneration", 1),
"context", Map.of(
"eventType", "google.storage.object.finalize")
));
HttpPost postRequest = new HttpPost(URI.create(functionUrl));
postRequest.setEntity(new StringEntity(jsonStr));
// 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)
.retryExceptions(HttpHostConnectException.class)
.intervalFunction(IntervalFunction.ofExponentialBackoff(200, 2))
.build());
Retry retry = registry.retry("my");
// Perform the request-retry process
CheckedRunnable retriableFunc = Retry.decorateCheckedRunnable(
retry, () -> client.execute(postRequest));
retriableFunc.run();
// Get Functions Framework plugin process' stdout
InputStream stdoutStream = emulatorProcess.getErrorStream();
ByteArrayOutputStream stdoutBytes = new ByteArrayOutputStream();
stdoutBytes.write(stdoutStream.readNBytes(stdoutStream.available()));
// Verify desired name value is present
assertThat(stdoutBytes.toString(StandardCharsets.UTF_8)).contains(
String.format("File: %s", name));
}
}
Puedes ejecutar las pruebas de integración para esta función de la siguiente manera:
Node.js
mocha test/sample.integration.storage.test.js --exit
Java
Maven usamvn verify
para ejecutar pruebas de integración:
mvn verify -Dit.test=ExampleIT
Pruebas de sistema
Estas son las pruebas de sistema para la función activada por Storage anterior:
Node.js
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const uuid = require('uuid');
const assert = require('assert');
const path = require('path');
const childProcess = require('child_process');
const moment = require('moment');
const promiseRetry = require('promise-retry');
// Use unique GCS filename to avoid conflicts between concurrent test runs
const gcsFileName = `test-${uuid.v4()}.txt`;
const localFileName = 'test.txt';
const bucketName = process.env.FUNCTIONS_DELETABLE_BUCKET;
const bucket = storage.bucket(bucketName);
const baseCmd = 'gcloud functions';
describe('system tests', () => {
it('helloGCS: should print event', async () => {
// Subtract time to work-around local-GCF clock difference
const startTime = moment().subtract(2, 'minutes').toISOString();
// Upload file
const filepath = path.join(__dirname, localFileName);
await bucket.upload(filepath, {
destination: gcsFileName,
});
// Wait for logs to become consistent
await promiseRetry(retry => {
const logs = childProcess
.execSync(`${baseCmd} logs read helloGCS --start-time ${startTime}`)
.toString();
try {
assert.ok(logs.includes(`File: ${gcsFileName}`));
assert.ok(logs.includes('Event Type: google.storage.object.finalize'));
} catch (err) {
retry(err);
}
});
});
Python
from datetime import datetime
from os import getenv, path
import subprocess
import time
import uuid
from google.cloud import storage
import pytest
PROJECT = getenv('GCP_PROJECT')
BUCKET = getenv('BUCKET')
assert PROJECT is not None
assert BUCKET is not None
@pytest.fixture(scope='module')
def storage_client():
yield storage.Client()
@pytest.fixture(scope='module')
def bucket_object(storage_client):
bucket_object = storage_client.get_bucket(BUCKET)
yield bucket_object
@pytest.fixture(scope='module')
def uploaded_file(bucket_object):
name = 'test-{}.txt'.format(str(uuid.uuid4()))
blob = bucket_object.blob(name)
test_dir = path.dirname(path.abspath(__file__))
blob.upload_from_filename(path.join(test_dir, 'test.txt'))
yield name
blob.delete()
def test_hello_gcs(uploaded_file):
start_time = datetime.utcnow().isoformat()
time.sleep(10) # Wait for logs to become consistent
log_process = subprocess.Popen([
'gcloud',
'alpha',
'functions',
'logs',
'read',
'hello_gcs_generic',
'--start-time',
start_time
], stdout=subprocess.PIPE)
logs = str(log_process.communicate()[0])
assert uploaded_file in logs
Go
package helloworld
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
"cloud.google.com/go/storage"
)
func TestHelloGCSSystem(t *testing.T) {
ctx := context.Background()
bucketName := os.Getenv("BUCKET_NAME")
client, err := storage.NewClient(ctx)
if err != nil {
t.Fatalf("storage.NewClient: %v", err)
}
// Create a file.
startTime := time.Now().UTC().Format(time.RFC3339)
oh := client.Bucket(bucketName).Object("TestHelloGCSSystem")
w := oh.NewWriter(ctx)
fmt.Fprintf(w, "Content of the file")
w.Close()
// Wait for logs to be consistent.
time.Sleep(20 * time.Second)
// Check logs.
want := "created"
if got := readLogs(t, startTime); !strings.Contains(got, want) {
t.Errorf("HelloGCS logged %q, want to contain %q", got, want)
}
// Modify the file.
startTime = time.Now().UTC().Format(time.RFC3339)
_, err = oh.Update(ctx, storage.ObjectAttrsToUpdate{
Metadata: map[string]string{"Content-Type": "text/html"},
})
if err != nil {
t.Errorf("Update: %v", err)
}
// Wait for logs to be consistent.
time.Sleep(20 * time.Second)
// Check logs.
want = "updated"
if got := readLogs(t, startTime); !strings.Contains(got, want) {
t.Errorf("HelloGCS logged %q, want to contain %q", got, want)
}
// Delete the file.
startTime = time.Now().UTC().Format(time.RFC3339)
if err := oh.Delete(ctx); err != nil {
t.Errorf("Delete: %v", err)
}
// Wait for logs to be consistent.
time.Sleep(20 * time.Second)
// Check logs.
want = "deleted"
if got := readLogs(t, startTime); !strings.Contains(got, want) {
t.Errorf("HelloGCS logged %q, want to contain %q", got, want)
}
}
func readLogs(t *testing.T, startTime string) string {
t.Helper()
cmd := exec.Command("gcloud", "alpha", "functions", "logs", "read", "HelloGCS", "--start-time", startTime)
got, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("exec.Command: %v", err)
}
return string(got)
}
Java
import static com.google.common.truth.Truth.assertThat;
import com.google.api.gax.paging.Page;
import com.google.cloud.logging.LogEntry;
import com.google.cloud.logging.Logging;
import com.google.cloud.logging.LoggingOptions;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
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.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.junit.BeforeClass;
import org.junit.Test;
public class ExampleSystemIT {
// TODO<developer>: set these values (as environment variables)
private static final String PROJECT_ID = System.getenv("GCP_PROJECT");
private static final String FUNCTIONS_BUCKET = System.getenv("FUNCTIONS_BUCKET");
private static final String FUNCTION_DEPLOYED_NAME = "HelloGcs";
private static final Storage STORAGE = StorageOptions.getDefaultInstance().getService();
private static Logging loggingClient;
private HelloGcs sampleUnderTest;
@BeforeClass
public static void setUp() throws IOException {
loggingClient = LoggingOptions.getDefaultInstance().getService();
}
private static String getLogEntriesAsString(String startTimestamp) {
// Construct Stackdriver logging filter
// See this page for more info: https://cloud.google.com/logging/docs/view/advanced-queries
String filter = "resource.type=\"cloud_function\""
+ " AND severity=INFO"
+ " AND resource.labels.function_name=" + FUNCTION_DEPLOYED_NAME
+ String.format(" AND timestamp>=\"%s\"", startTimestamp);
// Get Stackdriver logging entries
Page<LogEntry> logEntries =
loggingClient.listLogEntries(
Logging.EntryListOption.filter(filter),
Logging.EntryListOption.sortOrder(
Logging.SortingField.TIMESTAMP, Logging.SortingOrder.DESCENDING)
);
// Serialize Stackdriver logging entries + collect them into a single string
String logsConcat = StreamSupport.stream(logEntries.getValues().spliterator(), false)
.map((x) -> x.toString())
.collect(Collectors.joining("%n"));
return logsConcat;
}
@Test
public void helloGcs_shouldRunOnGcf() {
String filename = String.format("test-%s.txt", UUID.randomUUID());
// Subtract time to work-around local-GCF clock difference
Instant startInstant = Instant.now().minus(Duration.ofMinutes(4));
String startTimestamp = DateTimeFormatter.ISO_INSTANT.format(startInstant);
// Upload a file to Cloud Storage
BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(FUNCTIONS_BUCKET, filename)).build();
STORAGE.create(blobInfo);
// Keep retrying until the logs contain the desired invocation's log entry
// (If the invocation failed, the retry process will eventually time out)
String expected = String.format("File: %s", filename);
RetryRegistry registry = RetryRegistry.of(RetryConfig.custom()
.maxAttempts(8)
.intervalFunction(IntervalFunction.ofExponentialBackoff(1000, 2))
.retryOnResult(s -> !s.toString().contains(expected))
.build());
Retry retry = registry.retry(filename);
String logEntry = Retry
.decorateFunction(retry, ExampleSystemIT::getLogEntriesAsString)
.apply(startTimestamp);
// Perform final assertion (to make sure we fail on timeout)
assertThat(logEntry).contains(filename);
}
}
Implementa la función con el siguiente comando:
Node.js
gcloud functions deploy helloGCS \ --runtime nodejs10 \Puedes usar los siguientes valores para que la marca
--trigger-bucket YOUR_GCS_BUCKET_NAME
--runtime
especifique tu versión preferida de Node.js:nodejs10
nodejs12
nodejs14
(vista previa pública)
Python
gcloud functions deploy hello_gcs \ --runtime python38 \Puedes usar los siguientes valores para que la marca
--trigger-bucket YOUR_GCS_BUCKET_NAME
--runtime
especifique tu versión preferida de Python:python37
python38
python39
(vista previa pública)
Comienza a usarlo
gcloud functions deploy HelloGCS \ --runtime go113 \Puedes usar los siguientes valores en la marca
--trigger-bucket YOUR_GCS_BUCKET_NAME
--runtime
para especificar tu versión preferida de Go:
go111
(obsoleta)go113
Java
gcloud functions deploy java-hello-gcs \ --entry-point functions.HelloGcs \ --runtime java11 \ --memory 512MB \
--trigger-bucket YOUR_GCS_BUCKET_NAME
En este comando YOUR_GCS_BUCKET_NAME
es el bucket de Cloud Storage que quieres supervisar. Ten en cuenta que debe hacer referencia a un bucket que exista en el mismo proyecto de Cloud en el que se implementa la función.
Ejecuta las pruebas del sistema con los siguientes comandos:
Node.js
export BUCKET_NAME=YOUR_GCS_BUCKET_NAME mocha test/sample.system.storage.test.js --exit
Python
export BUCKET_NAME=YOUR_GCS_BUCKET_NAME pytest sample_storage_test_system.py
Go
export BUCKET_NAME=YOUR_GCS_BUCKET_NAME go test -v ./hello_cloud_storage_system_test.go
Java
Maven usamvn verify
para ejecutar pruebas de sistema:
mvn verify -Dit.test=ExampleSystemIT