Pub/Sub unit tests

Demonstrates how to unit test a function triggered by Pub/Sub.

Documentation pages that include this code sample

To view the code sample used in context, see the following documentation:

Code sample

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

C++

#include <google/cloud/functions/cloud_event.h>
#include <boost/log/core.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/shared_ptr.hpp>
#include <gmock/gmock.h>
#include <nlohmann/json.hpp>
#include <memory>
#include <sstream>

namespace gcf = ::google::cloud::functions;
extern void hello_world_pubsub(gcf::CloudEvent event);

namespace {

using ::testing::HasSubstr;

TEST(PubsubUnitTest, Basic) {
  auto core = boost::log::core::get();
  auto stream = boost::make_shared<std::ostringstream>();
  auto be = [core, stream]() {
    auto backend =
        boost::make_shared<boost::log::sinks::text_ostream_backend>();
    backend->add_stream(stream);

    // Enable auto-flushing after each log record written
    backend->auto_flush(true);
    using sink_t = boost::log::sinks::synchronous_sink<
        boost::log::sinks::text_ostream_backend>;
    auto be = boost::make_shared<sink_t>(backend);
    core->add_sink(be);
    return be;
  }();

  struct TestCases {
    std::string data;
    std::string expected;
  } cases[]{
      // The magic string was obtained using:
      //  /bin/echo -n 'C++' | openssl base64 -e
      {R"js({"message": {"data": "Qysr"}})js", "Hello C++"},
      {R"js({"message": {"data": ""}})js", "Hello World"},
  };

  for (auto const& test : cases) {
    SCOPED_TRACE("Testing for " + test.expected);
    gcf::CloudEvent event(
        /*id=*/"test-id-0001", /*source=*/"https://test-source.example.com",
        /*type=*/"google.cloud.pubsub.topic.v1.messagePublished");
    event.set_data_content_type("application/json");
    event.set_data(test.data);
    stream->str({});
    EXPECT_NO_THROW(hello_world_pubsub(event));
    auto log_lines = stream->str();
    EXPECT_THAT(log_lines, HasSubstr(test.expected));
  }

  core->remove_sink(be);
}

}  // namespace

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

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

PHP


namespace Google\Cloud\Samples\Functions\HelloworldPubsub\Test;

use Google\CloudFunctions\CloudEvent;
use PHPUnit\Framework\TestCase;

/**
 * Class SampleUnitTest.
 *
 * Unit test for 'Helloworld Pub/Sub' Cloud Function.
 */
class SampleUnitTest extends TestCase
{
    /**
     * Include the Cloud Function code before running any tests.
     *
     * @see https://phpunit.readthedocs.io/en/latest/fixtures.html
     */
    public static function setUpBeforeClass(): void
    {
        require_once __DIR__ . '/index.php';
    }

    public function dataProvider()
    {
        return [
            [
                'cloudevent' => CloudEvent::fromArray([
                    'id' => uniqid(),
                    'source' => 'pubsub.googleapis.com',
                    'specversion' => '1.0',
                    'type' => 'google.cloud.pubsub.topic.v1.messagePublished',
                    'data' => [
                        'data' => base64_encode('John')
                    ],
                ]),
                'expected' => 'Hello, John!'
            ],
        ];
    }

    /**
     * @dataProvider dataProvider
     */
    public function testFunction(
        CloudEvent $cloudevent,
        string $expected
    ): void {
        // Capture function output by overriding the function's logging behavior.
        // The 'LOGGER_OUTPUT' environment variable must be used in your function:
        //
        // $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
        // fwrite($log, 'Log Entry');
        putenv('LOGGER_OUTPUT=php://output');
        helloworldPubsub($cloudevent);
        // Provided by PHPUnit\Framework\TestCase.
        $actual = $this->getActualOutput();

        // Test that output includes the expected value.
        $this->assertStringContainsString($expected, $actual);
    }
}

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

Ruby

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

describe "functions_helloworld_pubsub" do
  include FunctionsFramework::Testing

  let(:resource_type) { "type.googleapis.com/google.pubsub.v1.PubsubMessage" }
  let(:source) { "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test" }
  let(:type) { "google.cloud.pubsub.topic.v1.messagePublished" }

  it "prints a name" do
    load_temporary "helloworld/pubsub/app.rb" do
      payload = { "@type" => resource_type, "message" => { "data" => Base64.encode64("Ruby") } }
      event = make_cloud_event payload, source: source, type: type
      _out, err = capture_subprocess_io do
        # Call tested function
        call_event "hello_pubsub", event
      end
      assert_match(/Hello, Ruby!/, err)
    end
  end

  it "prints hello world" do
    load_temporary "helloworld/pubsub/app.rb" do
      payload = { "@type" => resource_type, "message" => { "data" => nil } }
      event = make_cloud_event payload, source: source, type: type
      _out, err = capture_subprocess_io do
        # Call tested function
        call_event "hello_pubsub", event
      end
      assert_match(/Hello, World!/, err)
    end
  end
end

What's next

To search and filter code samples for other Google Cloud products, see the Google Cloud sample browser