Testing Functions

This guide covers writing unit tests for functions using the Functions Framework for Ruby. For more information about the Framework, see the Overview Guide.

Overview of function testing

One of the benefits of the functions-as-a-service paradigm is that functions are easy to test. In many cases, you can simply call a function with input, and test the output. You do not need to set up (or mock) an actual server.

The Functions Framework provides utility methods that streamline the process of setting up functions and the environment for testing, constructing input parameters, and interpreting results. These are available in the FunctionsFramework::Testing module. Generally, you can include this module in your Minitest test class or RSpec describe block.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing
  # define tests...
end
require "rspec"
require "functions_framework/testing"

describe "My functions" do
  include FunctionsFramework::Testing
  # define examples...
end

Loading functions for testing

To test a function, you'll need to load the Ruby file that defines the function, and run the function to test its results. The Testing module provides a method FunctionsFramework::Testing#load_temporary, which loads a Ruby file, defining functions but only for the scope of your test. This allows your test to coexist with tests for other functions, even functions with the same name from a different Ruby file.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing

  def test_a_function
    load_temporary "foo.rb" do
      # Test a function defined in foo.rb
    end
  end

  def test_another_function
    load_temporary "bar.rb" do
      # Test a function defined in bar.rb
    end
  end
end

When running a test suite, you'll typically need to load all the Ruby files that define your functions. While load_temporary can ensure that the function definitions do not conflict, it cannot do the same for classes, methods, and other Ruby constructs. So, for testability, it is generally good practice to include only functions in one of these files. If you need to write supporting helper methods, classes, constants, or other code, include them in separate ruby files that you require.

Testing HTTP functions

Testing an HTTP function is generally as simple as generating a request, calling the function, and asserting against the response.

The input to an HTTP function is a Rack::Request object. It is usually not hard to construct one of these objects, but the Testing module includes helper methods that you can use to create simple requests for many basic cases.

When you have constructed an input request, use FunctionsFramework::Testing#call_http to call a named function, passing the request object. This method returns a Rack::Response that you can assert against.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing

  def test_http_function
    load_temporary "app.rb" do
      request = make_post_request "https://example.com/foo", "{\"name\":\"Ruby\"}",
                                  ["Content-Type: application/json"]
      response = call_http "my_function", request
      assert_equal 200, response.status
      assert_equal "Hello, Ruby!", response.body.join
    end
  end
end

If the function raises an exception, the exception will be converted to a 500 response object. So if you are testing an error case, you should still check the response object rather than looking for a raised exception.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing

  def test_erroring_http_function
    load_temporary "app.rb" do
      request = make_post_request "https://example.com/foo", "{\"name\":\"Ruby\"}",
                                  ["Content-Type: application/json"]
      response = call_http "error_function", request
      assert_equal 500, response.status
      assert_match(/ArgumentError/, response.body.join)
    end
  end
end

Testing CloudEvent functions

Testing a CloudEvent function works similarly. The Testing module provides methods to help construct example CloudEvent objects, which can then be passed to the method FunctionsFramework::Testing#call_event.

Unlike HTTP functions, event functions do not have a return value. Instead, you will need to test side effects. A common approach is to test logs by capturing the standard error output.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing

  def test_event_function
    load_temporary "app.rb" do
      event = make_cloud_event "Hello, world!", type: "my-type"
      _out, err = capture_subprocess_io do
        call_event "my_function", event
      end
      assert_match(/Received: "Hello, world!"/, err)
    end
  end
end

Testing startup tasks

When a functions server is starting up, it calls startup tasks automatically. In the testing environment, when you call a function using the FunctionsFramework::Testing#call_http or FunctionsFramework::Testing#call_event methods, the testing environment will also automatically execute any startup tasks for you.

You can also call startup tasks explicitly to test them in isolation, using the FunctionsFramework::Testing#run_startup_tasks method. Pass the name of a function, and the testing module will execute all defined startup blocks, in order, as if the server were preparing that function for execution. FunctionsFramework::Testing#run_startup_tasks returns the resulting globals as a hash, so you can assert against its contents.

If you use FunctionsFramework::Testing#run_startup_tasks to run the startup tasks explicitly, they will not be run again when you call the function itself using FunctionsFramework::Testing#call_http or FunctionsFramework::Testing#call_event. However, if startup tasks have already been run implicitly by FunctionsFramework::Testing#call_http or FunctionsFramework::Testing#call_event, then attempting to run them again explicitly by calling FunctionsFramework::Testing#run_startup_tasks will result in an exception.

There is currently no way to run a single startup block in isolation. If you have multiple startup blocks defined, they are always executed together.

Following is an example test that runs startup tasks explicitly and asserts against the effect on the globals.

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

class MyTest < Minitest::Test
  include FunctionsFramework::Testing

  def test_startup_tasks
    load_temporary "app.rb" do
      globals = run_startup_tasks "my_function"
      assert_equal "foo", globals[:my_global]

      request = make_get_request "https://example.com/foo"
      response = call_http "my_function", request
      assert_equal 200, response.status
    end
  end
end