module FunctionsFramework::Testing
Helpers for writing unit tests.
Methods on this module can be called as module methods, or this module can be included in a test class.
## Example
Suppose we have the following app that uses the functions framework:
# app.rb require "functions_framework" FunctionsFramework.http "my-function" do |request| "Hello, world!" end
The following is a test that could be run against that app:
# test_app.rb require "minitest/autorun" require "functions_framework/testing" class MyTest < Minitest::Test # Make the testing methods available. include FunctionsFramework::Testing def test_my_function # Load app.rb and apply its functions within this block load_temporary "app.rb" do # Create a mock http (rack) request request = make_get_request "http://example.com" # Call the function and get a rack response response = call_http "my-function", request # Assert against the response assert_equal "Hello, world!", response.body.join end end end
Public Class Methods
@private
# File lib/functions_framework/testing.rb, line 362 def build_standard_env url, headers env = { ::Rack::SCRIPT_NAME => "", ::Rack::PATH_INFO => url.path, ::Rack::QUERY_STRING => url.query, ::Rack::SERVER_NAME => url.host, ::Rack::SERVER_PORT => url.port, ::Rack::RACK_URL_SCHEME => url.scheme, ::Rack::RACK_VERSION => ::Rack::VERSION, ::Rack::RACK_LOGGER => ::FunctionsFramework.logger, ::Rack::RACK_INPUT => ::StringIO.new, ::Rack::RACK_ERRORS => ::StringIO.new } headers.each do |header| case header when String name, value = header.split ":" when ::Array name, value = header end next unless name && value name = name.strip.upcase.tr "-", "_" name = "HTTP_#{name}" unless ["CONTENT_TYPE", "CONTENT_LENGTH"].include? name env[name] = value.strip end env end
@private
# File lib/functions_framework/testing.rb, line 309 def current_globals name, globals = nil name = name.to_s globals_by_name = ::Thread.current[:functions_framework_testing_globals] || @main_globals if globals globals_by_name[name] = globals else globals_by_name[name] end end
@private
# File lib/functions_framework/testing.rb, line 303 def current_registry ::Thread.current[:functions_framework_testing_registry] || ::FunctionsFramework.global_registry end
@private
# File lib/functions_framework/testing.rb, line 320 def interpret_response response = begin yield rescue ::StandardError => e e end case response when ::Rack::Response response when ::Array ::Rack::Response.new response[2], response[0], response[1] when ::String string_response response, 200 when ::Hash json = ::JSON.dump response string_response json, 200, content_type: "application/json" when ::StandardError message = "#{response.class}: #{response.message}\n#{response.backtrace}\n" string_response message, 500 else raise "Unexpected response type: #{response.inspect}" end end
@private
# File lib/functions_framework/testing.rb, line 281 def load_for_testing path old_registry = ::FunctionsFramework.global_registry ::Thread.current[:functions_framework_testing_registry] = @mutex.synchronize do if @testing_registries.key? path ::FunctionsFramework.global_registry = @testing_registries[path] else new_registry = ::FunctionsFramework::Registry.new ::FunctionsFramework.global_registry = new_registry ::Kernel.load path @testing_registries[path] = new_registry end end ::Thread.current[:functions_framework_testing_globals] = {} yield ensure ::Thread.current[:functions_framework_testing_registry] = nil ::Thread.current[:functions_framework_testing_globals] = nil ::FunctionsFramework.global_registry = old_registry end
@private
# File lib/functions_framework/testing.rb, line 346 def string_response string, status, content_type: nil string.force_encoding ::Encoding::ASCII_8BIT unless string.valid_encoding? if string.encoding == ::Encoding::ASCII_8BIT content_type ||= "application/octet-stream" else content_type ||= "text/plain" content_type = "#{content_type}; charset=#{string.encoding.name.downcase}" end headers = { "Content-Type" => content_type, "Content-Length" => string.bytesize } ::Rack::Response.new string, status, headers end
Public Instance Methods
Call the given event function for testing. The underlying function must be of type :cloud_event`.
By default, the startup tasks will be run for the given function if they have not already been run. You can, however, disable running startup tasks by providing an explicit globals hash.
By default, the {FunctionsFramework.logger} will be used, but you can override that by providing your own logger. In particular, to disable logging, you can pass `Logger.new(nil)`.
@param name [String] The name of the function to call @param event [::CloudEvents::Event] The event to send @param globals [Hash] Do not run startup tasks, and instead provide the
globals directly. Optional.
@param logger [Logger] Use the given logger instead of the Functions
Framework's global logger. Optional.
@return [nil]
# File lib/functions_framework/testing.rb, line 170 def call_event name, event, globals: nil, logger: nil globals ||= run_startup_tasks name, logger: logger, lenient: true function = Testing.current_registry[name] case function&.type when :cloud_event function.call event, globals: globals, logger: logger nil when nil raise "Unknown function name #{name}" else raise "Function #{name} is not a CloudEvent function" end end
Call the given HTTP function for testing. The underlying function must be of type `:http`. Returns the Rack response.
By default, the startup tasks will be run for the given function if they have not already been run. You can, however, disable running startup tasks by providing an explicit globals hash.
By default, the {FunctionsFramework.logger} will be used, but you can override that by providing your own logger. In particular, to disable logging, you can pass `Logger.new(nil)`.
@param name [String] The name of the function to call @param request [Rack::Request] The Rack request to send @param globals [Hash] Do not run startup tasks, and instead provide the
globals directly. Optional.
@param logger [Logger] Use the given logger instead of the Functions
Framework's global logger. Optional.
@return [Rack::Response]
# File lib/functions_framework/testing.rb, line 135 def call_http name, request, globals: nil, logger: nil globals ||= run_startup_tasks name, logger: logger, lenient: true function = Testing.current_registry[name] case function&.type when :http Testing.interpret_response do function.call request, globals: globals, logger: logger end when nil raise "Unknown function name #{name}" else raise "Function #{name} is not an HTTP function" end end
Load the given functions source for the duration of the given block, and restore the previous status afterward.
@param path [String] File path to load
# File lib/functions_framework/testing.rb, line 73 def load_temporary path, &block path = ::File.expand_path path Testing.load_for_testing path, &block end
Make a simple CloudEvent, for passing to a function test. The event data is required, but all other parameters are optional (i.e. a reasonable or random value will be generated if not provided).
@param data [Object] The data @param id [String] Event ID (optional) @param source [String,URI] Event source (optional) @param type [String] Event type (optional) @param spec_version [String] Spec version (optional) @param data_content_type [String,::CloudEvents::ContentType]
Content type for the data (optional)
@param data_schema [String,URI] Data schema (optional) @param subject [String] Subject (optional) @param time [String,DateTime] Event timestamp (optional) @return [::CloudEvents::Event]
# File lib/functions_framework/testing.rb, line 249 def make_cloud_event data, id: nil, source: nil, type: nil, spec_version: nil, data_content_type: nil, data_schema: nil, subject: nil, time: nil id ||= "random-id-#{rand 100_000_000}" source ||= "functions-framework-testing" type ||= "com.example.test" spec_version ||= "1.0" ::CloudEvents::Event.new id: id, source: source, type: type, spec_version: spec_version, data_content_type: data_content_type, data_schema: data_schema, subject: subject, time: time, data: data end
Make a simple GET request, for passing to a function test.
@param url [URI,String] The URL to get. @param headers [Array,Hash] HTTP headers. May be given as a hash (of
header names mapped to values), an array of strings (where each string is of the form `Header-Name: Header value`), or an array of two-element string arrays.
@return [Rack::Request]
# File lib/functions_framework/testing.rb, line 213 def make_get_request url, headers = [] make_request url, headers: headers end
Make a simple POST request, for passing to a function test.
@param url [URI,String] The URL to post to. @param body [String] The body to post. @param headers [Array,Hash] HTTP headers. May be given as a hash (of
header names mapped to values), an array of strings (where each string is of the form `Header-Name: Header value`), or an array of two-element string arrays.
@return [Rack::Request]
# File lib/functions_framework/testing.rb, line 228 def make_post_request url, body, headers = [] make_request url, method: ::Rack::POST, body: body, headers: headers end
Make a Rack request, for passing to a function test.
@param url [URI,String] The URL to get, including query params. @param method [String] The HTTP method (defaults to “GET”). @param body [String] The HTTP body, if any. @param headers [Array,Hash] HTTP headers. May be given as a hash (of
header names mapped to values), an array of strings (where each string is of the form `Header-Name: Header value`), or an array of two-element string arrays.
@return [Rack::Request]
# File lib/functions_framework/testing.rb, line 196 def make_request url, method: ::Rack::GET, body: nil, headers: [] env = Testing.build_standard_env URI(url), headers env[::Rack::REQUEST_METHOD] = method env[::Rack::RACK_INPUT] = ::StringIO.new body if body ::Rack::Request.new env end
Run startup tasks for the given function name and return the initialized globals hash.
Normally, this will be run automatically prior to the first call to the function using {call_http} or {call_event}, if it has not already been run. However, you can call it explicitly to test its behavior. It cannot be called more than once for any given function.
By default, the {FunctionsFramework.logger} will be used, but you can override that by providing your own logger. In particular, to disable logging, you can pass `Logger.new(nil)`.
@param name [String] The name of the function to start up. @param logger [Logger] Use the given logger instead of the Functions
Framework's global logger. Optional.
@param lenient [Boolean] If false (the default), raise an error if the
given function has already had its startup tasks run. If true, duplicate requests to run startup tasks are ignored.
@return [Hash] The initialized globals.
# File lib/functions_framework/testing.rb, line 99 def run_startup_tasks name, logger: nil, lenient: false function = Testing.current_registry[name] raise "Unknown function name #{name}" unless function globals = Testing.current_globals name if globals raise "Function #{name} has already been started up" unless lenient else globals = function.populate_globals Testing.current_registry.startup_tasks.each do |task| task.call function, globals: globals, logger: logger end Testing.current_globals name, globals end globals.freeze end