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