python-docs-samples icon indicating copy to clipboard operation
python-docs-samples copied to clipboard

Possible improvements to GCF integration tests

Open di opened this issue 5 years ago • 5 comments

In which file did you encounter the issue?

https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/functions/helloworld/sample_http_test_integration.py

Describe the issue

Based on issues described in https://github.com/GoogleCloudPlatform/functions-framework-python/issues/97, there is a simpler / less brittle integration test we could prescribe here (that is used by the Function Framework's own test suite as well.)

Instead of starting background processes, the framework provides a test client that can directly invoke the underlying WSGI application.

Instead of:

def test_args():
    name = str(uuid.uuid4())
    port = 8080  # Each functions framework instance needs a unique por

    process = subprocess.Popen(
      [
        'functions-framework',
        '--target', 'hello_http',
        '--port', str(port)
      ],
      cwd=os.path.dirname(__file__),
      stdout=subprocess.PIPE
    )

    # Send HTTP request simulating Pub/Sub message
    # (GCF translates Pub/Sub messages to HTTP requests internally)
    BASE_URL = os.getenv('BASE_URL')

    retry_policy = Retry(total=6, backoff_factor=1)
    retry_adapter = requests.adapters.HTTPAdapter(
      max_retries=retry_policy)

    session = requests.Session()
    session.mount(BASE_URL, retry_adapter)

    name = str(uuid.uuid4())
    res = requests.post(
      BASE_URL,
      json={'name': name}
    )
    assert res.text == 'Hello {}!'.format(name)

    # Stop the functions framework process
    process.kill()
    process.wait()

This would become something like the following (untested):

def test_args():
    name = str(uuid.uuid4())
    target = "hello_http"
    source = os.path.dirname(__file__) / "main.py"
    client = create_app(target, source).test_client()

    resp = client.post("/my_path", json={'name': name})
    assert resp.status_code == 200
    assert resp.data == b'Hello {}!'.format(name)

This avoids invoking gunicorn/flask's HTTP servers entirely, which is arguably makes this a less "complete" integration test, but probably more ergonomic (and should produce the same results) as what is already suggested.

(cc @ace-n)

di avatar Nov 06 '20 19:11 di

Got it working!

import flask
from functions_framework import create_app


def test_integration():
    app = create_app('my_function')
    app.testing = True  # bubble exceptions
    client = app.test_client()
    res: flask.response = client.post('/', json={'hello': 'world'})
    assert res.status_code == 200

I get how people might want to perform their integration tests with a language agnostic tool. But for me this is perfect :) Thanks @di

dinigo avatar Nov 08 '20 13:11 dinigo

@di happy to make this change if you and @grant are both in favor. 🙂

ace-n avatar Dec 08 '20 04:12 ace-n

@ace-n Sounds like a python GCF testing best practice we can add to our python samples.

grant avatar Dec 08 '20 04:12 grant

I got back here to get this piece of code again. I think this belongs in the oficial docs

dinigo avatar May 19 '21 10:05 dinigo

Unassigning this, as I'm quite swamped at the moment.

(This may be a good candidate for a future bug burn-down.)

ace-n avatar Jan 13 '22 00:01 ace-n