Package: cloud-functions-test
cloud-functions-testTest GCP Cloud Functions locallyTable of ContentsBasic DescriptionInstallationUsing cloud-functions-testGeneral FunctioningHttp-triggered FunctionsWildcards for Expected ContentEvent-triggered FunctionsSettingsContributingContactBasic DescriptionThe objective of this package is to allow you to easily test your GCP Cloud Functions locally before deploying them. The package relies on Google's functions-framework to launch a local server that receives requests triggering your Cloud Function. Even though the functions are triggered through a request in the package's testing mechanism, event-triggered Cloud Functions are also supported.
The package's interface is intended to look similar to that of popular python unit-testing libraries. You can define different tests as classes in a python file, specifying the input and the expected output. You can then launch the tests from the cli and the results are printed unto your terminal.Let's say your Cloud Function is defined in the following filemain.pywith an entrypoint functionmaindefmain(request):value=request.get_json()if"a"notinvalue:return("Error",400)return({**value,"b":2},200)You can define your tests in a file calledcf_tests.pyin the same directoryclassCorrectInput:data={"a":1}headers={'Content-Type':'application/json'}status_code=200output={"a":1,"b":2}classIncorrectInput:data={1:2}headers={'Content-Type':'application/json'}status_code=400classCrashInput:data=["a"]headers={'Content-Type':'application/json'}error=TrueclassOopsUnintendedError:data={"a":"1"}headers={'Content-Type':'application/json'}status_code=200output={"a":1,"b":2}You can then launch the tests by entering in your terminalcloud-functions-testYou would see the following output in your terminal (with colors):================================================ Running 4 tests from the cf_tests module... =================================================
test CorrectInput in 0.009216s:  PASSED
test IncorrectInput in 0.002605s:  PASSED
test CrashInput in 0.007339s:  PASSED
test OopsUnintendedError in 0.002422s:  FAILED
*** 3 tests passed and 1 failed ***
=================================================================== FAILED ===================================================================
test OopsUnintendedError
Unexpected output
- expected: {'a': 1, 'b': 2}
- received: {'a': '1', 'b': 2}InstallationTo install the latest version of the package:pipinstallcloud-functions-testUsing cloud-functions-testGeneral FunctioningIn your test module,cf_tests.pyby default, you need to create a basic class for each of your test.Each class can have a set of attributes that work as parameters for the test. All of those attributes are optional. If none are specified, the test will return "PASSED" if the Cloud Function ran without crashing.If you do specifiy some of those attributes, you need to make sure their value is of a supported type.There are 2 possible attributes that are common to both http-triggered and event-triggerd functions:error(bool): indicates whether the test is expected to raise an Exception. The test will succeed if the function crashes while it will fail if it runs without errordisplay_logs(bool): indicates whether the logs and the return value should be displayed even in case of success (they are always displayed in case of failure of the test)Http-triggered FunctionsThis package has primarily been made for http-triggered Cloud Function so your test classes will be considered to be for this kind of function by default.You can specify 4 additional attributes for those:data(dict, list): the payload that will be included in the request triggering your functionheaders(dict): the headers that will be included in the request triggering your functionstatus_code(dict, list): the status code your function is expected to return giving the parameters providedoutput(dict, list): the output your function is expected to return giving the parameters provided. Details on the specific structure the value of this attribute can take are specified belowWildcards for Expected ContentIt's quite common to want to check the validity of the output of an http-triggered Cloud Function without knowing the exact value of the expected output. This happens for instance if you Cloud Function is calling an external service or if your output includes an "updated_at" timestamp. In that case, you may want to test that the structure of the output is correct rather than the exact content.There are 3 types of wildcards your can use for that purpose:EllipsisIf you include this object in a list, the test will only check that the other elements of the list are found in the actual outputA:
    output = ["a", Ellipsis]This will match withactual_output = ["a", "b", "c"]If you pass this object as a key in a dict, the test will only check that all other key/value pairs are found in the actual outputA:
    output = {"a": 1, Ellipsis: Ellipsis}This will match withactual_output = {"a": 1, "b": 2, "c": 3}TypesYou can include both basic/collection types (int, float, str, list, dict) and types from the typing package (Any, Union, List, Tuple, Dict) in the expected output.Simple caseA:
    output = List[int]This will match withactual_output = [1, 2]More complex exampleA:
    output = Tuple[dict, List[int], Union[int, str]]This will match withactual_output = [{"a": 1}, [1, 2], "b"]RegexYou can include an object of type re.Pattern in your expected output. If the actual output is a string, it will be matched with the pattern.A:
    output = ["a", re.compile(r'^\d+$')]This will match withactual_output = ["a", "123a"]Event-triggered FunctionsTesting event-triggered functions is not as straightforward as for http-triggered ones. Since we cannot fully simulate the underlying event for which GCP would trigger the function, we make a request to it as a workaround. In the definition of your test class, you can specify the event and the context you want your function to receive as dictionaries. The package will then pass them on your function.You can specify 2 additional attributes for those:event(dict): the event that your function will receivecontext(dict): the context that you function will receiveSettingsThere are 5 options you can modify for running your tests. Those can typically be modified either in your test module or in the climodule: defaults to "cf_tests.py", name of the module in which your test classes are definedcli:cloud-functions-test --module <name_test_module>source: defaults to "main.py", path to the file in which your Cloud Function is definedcli:cloud-functions-test --source <path_to_file>in test module:import cloud_function_framework
cloud_function_framework.source = "<path_to_file>"entrypoint: defaults to "main", name of the Cloud Function entrypoint in the file in which your Cloud Function is definedcli:cloud-functions-test --entrypoint <entrypoint>in test module:import cloud_function_framework
cloud_function_framework.entrypoint = "<entrypoint>"env: defaults to ".env", path to the file in which your environment variables are defined (nothing happens if the file does not exist)cli:cloud-functions-test --env <env_file_path>in test module:import cloud_function_framework
cloud_function_framework.env = "<env_file_path>"This file can either be a bash file organized as such:ENV=devFOO=barOr a Terraform file. In this case, the package will do its best to infer the env variables from the structure of the file. Example of supported structure:module"test_module"{env=var.envenvironment_variables={ENV=var.env,LOCATION_ID="europe-west1",NAME="test_${var.env}",}memory_mb=512}port: defaults to 8080, port of localhost that will be used by functions-framework to launch the local server receiving the requests that will trigger the Cloud Functionscli:cloud-functions-test --port <port>in test module:import cloud_function_framework
cloud_function_framework.port = <port>ContributingYour contribution is very much welcome, both through the creation of issues and through the opening of pull requests. To open a pull request:Fork the repoCreate your feature branch (git checkout -b my-new-feature)Commit your changes (git commit -m 'Add some feature')Push to the branch (git push origin my-new-feature)Create a new Pull RequestContactYou can reach out to me at:robin.picard@sciencespo.fr