Skip to content

Commit

Permalink
Refactor web_api test using pytest.fixtures
Browse files Browse the repository at this point in the history
Merge pull request #984 from openfisca/refactor-web_api-test-fixtures
  • Loading branch information
Mauko Quiroga committed Apr 16, 2021
2 parents 7566d96 + 0e42b47 commit 3123d27
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 232 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

### 35.3.6 [#984](https://github.com/openfisca/openfisca-core/pull/984)

#### Technical changes

- In web_api tests, extract `test_client` to a fixture reusable by all the tests in the test suite.
- To mitigate possible performance issues, by default the fixture is initialised once per test module.
- This follows the same approach as [#997](https://github.com/openfisca/openfisca-core/pull/997)


### 35.3.5 [#997](https://github.com/openfisca/openfisca-core/pull/997)

#### Technical changes
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pytest_plugins = [
"tests.fixtures.appclient",
"tests.fixtures.entities",
"tests.fixtures.simulations",
"tests.fixtures.taxbenefitsystems",
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

setup(
name = 'OpenFisca-Core',
version = '35.3.5',
version = '35.3.6',
author = 'OpenFisca Team',
author_email = 'contact@openfisca.org',
classifiers = [
Expand Down
31 changes: 31 additions & 0 deletions tests/fixtures/appclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pytest

from openfisca_web_api import app


@pytest.fixture(scope="module")
def test_client(tax_benefit_system):
""" This module-scoped fixture creates an API client for the TBS defined in the `tax_benefit_system`
fixture. This `tax_benefit_system` is mutable, so you can add/update variables. Example:
```
from openfisca_country_template import entities
from openfisca_core import periods
from openfisca_core.variables import Variable
...
class new_variable(Variable):
value_type = float
entity = entities.Person
definition_period = periods.MONTH
label = "New variable"
reference = "https://law.gov.example/new_variable" # Always use the most official source
tax_benefit_system.add_variable(new_variable)
flask_app = app.create_app(tax_benefit_system)
```
"""

# Create the test API client
flask_app = app.create_app(tax_benefit_system)
return flask_app.test_client()
6 changes: 0 additions & 6 deletions tests/web_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
# -*- coding: utf-8 -*-

import pkg_resources
from openfisca_web_api.app import create_app
from openfisca_core.scripts import build_tax_benefit_system

TEST_COUNTRY_PACKAGE_NAME = 'openfisca_country_template'
distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME)
tax_benefit_system = build_tax_benefit_system(TEST_COUNTRY_PACKAGE_NAME, extensions = None, reforms = None)
subject = create_app(tax_benefit_system).test_client()
127 changes: 61 additions & 66 deletions tests/web_api/test_calculate.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
# -*- coding: utf-8 -*-

import os
import copy
import dpath
import json
from http.client import OK, BAD_REQUEST, NOT_FOUND, INTERNAL_SERVER_ERROR
from copy import deepcopy

from http import client
import os
import pytest
import dpath

from openfisca_country_template.situation_examples import couple

from . import subject


def post_json(data = None, file = None):
def post_json(client, data = None, file = None):
if file:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', file)
with open(file_path, 'r') as file:
data = file.read()
return subject.post('/calculate', data = data, content_type = 'application/json')
return client.post('/calculate', data = data, content_type = 'application/json')


def check_response(data, expected_error_code, path_to_check, content_to_check):
response = post_json(data)
def check_response(client, data, expected_error_code, path_to_check, content_to_check):
response = post_json(client, data)
assert response.status_code == expected_error_code
json_response = json.loads(response.data.decode('utf-8'))
if path_to_check:
Expand All @@ -31,32 +26,32 @@ def check_response(data, expected_error_code, path_to_check, content_to_check):


@pytest.mark.parametrize("test", [
('{"a" : "x", "b"}', BAD_REQUEST, 'error', 'Invalid JSON'),
('["An", "array"]', BAD_REQUEST, 'error', 'Invalid type'),
('{"persons": {}}', BAD_REQUEST, 'persons', 'At least one person'),
('{"persons": {"bob": {}}, "unknown_entity": {}}', BAD_REQUEST, 'unknown_entity', 'entities are not found',),
('{"persons": {"bob": {}}, "households": {"dupont": {"parents": {}}}}', BAD_REQUEST, 'households/dupont/parents', 'type',),
('{"persons": {"bob": {"unknown_variable": {}}}}', NOT_FOUND, 'persons/bob/unknown_variable', 'You tried to calculate or to set',),
('{"persons": {"bob": {"housing_allowance": {}}}}', BAD_REQUEST, 'persons/bob/housing_allowance', "You tried to compute the variable 'housing_allowance' for the entity 'persons'",),
('{"persons": {"bob": {"salary": 4000 }}}', BAD_REQUEST, 'persons/bob/salary', 'period',),
('{"persons": {"bob": {"salary": {"2017-01": "toto"} }}}', BAD_REQUEST, 'persons/bob/salary/2017-01', 'expected type number',),
('{"persons": {"bob": {"salary": {"2017-01": {}} }}}', BAD_REQUEST, 'persons/bob/salary/2017-01', 'expected type number',),
('{"persons": {"bob": {"age": {"2017-01": "toto"} }}}', BAD_REQUEST, 'persons/bob/age/2017-01', 'expected type integer',),
('{"persons": {"bob": {"birth": {"2017-01": "toto"} }}}', BAD_REQUEST, 'persons/bob/birth/2017-01', 'Can\'t deal with date',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["unexpected_person_id"]}}}', BAD_REQUEST, 'households/household/parents', 'has not been declared in persons',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", "bob"]}}}', BAD_REQUEST, 'households/household/parents', 'has been declared more than once',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", {}]}}}', BAD_REQUEST, 'households/household/parents/1', 'Invalid type',),
('{"persons": {"bob": {"salary": {"invalid period": 2000 }}}}', BAD_REQUEST, 'persons/bob/salary', 'Expected a period',),
('{"persons": {"bob": {"salary": {"invalid period": null }}}}', BAD_REQUEST, 'persons/bob/salary', 'Expected a period',),
('{"persons": {"bob": {"basic_income": {"2017": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', BAD_REQUEST, 'persons/bob/basic_income/2017', '"basic_income" can only be set for one month',),
('{"persons": {"bob": {"salary": {"ETERNITY": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', BAD_REQUEST, 'persons/bob/salary/ETERNITY', 'salary is only defined for months',),
('{"persons": {"alice": {}, "bob": {}, "charlie": {}}, "households": {"_": {"parents": ["alice", "bob", "charlie"]}}}', BAD_REQUEST, 'households/_/parents', 'at most 2 parents in a household',),
('{"a" : "x", "b"}', client.BAD_REQUEST, 'error', 'Invalid JSON'),
('["An", "array"]', client.BAD_REQUEST, 'error', 'Invalid type'),
('{"persons": {}}', client.BAD_REQUEST, 'persons', 'At least one person'),
('{"persons": {"bob": {}}, "unknown_entity": {}}', client.BAD_REQUEST, 'unknown_entity', 'entities are not found',),
('{"persons": {"bob": {}}, "households": {"dupont": {"parents": {}}}}', client.BAD_REQUEST, 'households/dupont/parents', 'type',),
('{"persons": {"bob": {"unknown_variable": {}}}}', client.NOT_FOUND, 'persons/bob/unknown_variable', 'You tried to calculate or to set',),
('{"persons": {"bob": {"housing_allowance": {}}}}', client.BAD_REQUEST, 'persons/bob/housing_allowance', "You tried to compute the variable 'housing_allowance' for the entity 'persons'",),
('{"persons": {"bob": {"salary": 4000 }}}', client.BAD_REQUEST, 'persons/bob/salary', 'period',),
('{"persons": {"bob": {"salary": {"2017-01": "toto"} }}}', client.BAD_REQUEST, 'persons/bob/salary/2017-01', 'expected type number',),
('{"persons": {"bob": {"salary": {"2017-01": {}} }}}', client.BAD_REQUEST, 'persons/bob/salary/2017-01', 'expected type number',),
('{"persons": {"bob": {"age": {"2017-01": "toto"} }}}', client.BAD_REQUEST, 'persons/bob/age/2017-01', 'expected type integer',),
('{"persons": {"bob": {"birth": {"2017-01": "toto"} }}}', client.BAD_REQUEST, 'persons/bob/birth/2017-01', 'Can\'t deal with date',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["unexpected_person_id"]}}}', client.BAD_REQUEST, 'households/household/parents', 'has not been declared in persons',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", "bob"]}}}', client.BAD_REQUEST, 'households/household/parents', 'has been declared more than once',),
('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", {}]}}}', client.BAD_REQUEST, 'households/household/parents/1', 'Invalid type',),
('{"persons": {"bob": {"salary": {"invalid period": 2000 }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'Expected a period',),
('{"persons": {"bob": {"salary": {"invalid period": null }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'Expected a period',),
('{"persons": {"bob": {"basic_income": {"2017": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', client.BAD_REQUEST, 'persons/bob/basic_income/2017', '"basic_income" can only be set for one month',),
('{"persons": {"bob": {"salary": {"ETERNITY": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', client.BAD_REQUEST, 'persons/bob/salary/ETERNITY', 'salary is only defined for months',),
('{"persons": {"alice": {}, "bob": {}, "charlie": {}}, "households": {"_": {"parents": ["alice", "bob", "charlie"]}}}', client.BAD_REQUEST, 'households/_/parents', 'at most 2 parents in a household',),
])
def test_responses(test):
check_response(*test)
def test_responses(test_client, test):
check_response(test_client, *test)


def test_basic_calculation():
def test_basic_calculation(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {
Expand Down Expand Up @@ -101,8 +96,8 @@ def test_basic_calculation():
}
})

response = post_json(simulation_json)
assert response.status_code == OK
response = post_json(test_client, simulation_json)
assert response.status_code == client.OK
response_json = json.loads(response.data.decode('utf-8'))
assert dpath.get(response_json, 'persons/bill/basic_income/2017-12') == 600 # Universal basic income
assert dpath.get(response_json, 'persons/bill/income_tax/2017-12') == 300 # 15% of the salary
Expand All @@ -112,7 +107,7 @@ def test_basic_calculation():
assert dpath.get(response_json, 'households/first_household/housing_tax/2017') == 3000


def test_enums_sending_identifier():
def test_enums_sending_identifier(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {}
Expand All @@ -133,13 +128,13 @@ def test_enums_sending_identifier():
}
})

response = post_json(simulation_json)
assert response.status_code == OK
response = post_json(test_client, simulation_json)
assert response.status_code == client.OK
response_json = json.loads(response.data.decode('utf-8'))
assert dpath.get(response_json, 'households/_/housing_tax/2017') == 0


def test_enum_output():
def test_enum_output(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {},
Expand All @@ -154,13 +149,13 @@ def test_enum_output():
}
})

response = post_json(simulation_json)
assert response.status_code == OK
response = post_json(test_client, simulation_json)
assert response.status_code == client.OK
response_json = json.loads(response.data.decode('utf-8'))
assert dpath.get(response_json, "households/_/housing_occupancy_status/2017-01") == "tenant"


def test_enum_wrong_value():
def test_enum_wrong_value(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {},
Expand All @@ -175,15 +170,15 @@ def test_enum_wrong_value():
}
})

response = post_json(simulation_json)
assert response.status_code == BAD_REQUEST
response = post_json(test_client, simulation_json)
assert response.status_code == client.BAD_REQUEST
response_json = json.loads(response.data.decode('utf-8'))
message = "Possible values are ['owner', 'tenant', 'free_lodger', 'homeless']"
text = dpath.get(response_json, "households/_/housing_occupancy_status/2017-01")
assert message in text


def test_encoding_variable_value():
def test_encoding_variable_value(test_client):
simulation_json = json.dumps({
"persons": {
"toto": {}
Expand All @@ -202,15 +197,15 @@ def test_encoding_variable_value():
})

# No UnicodeDecodeError
response = post_json(simulation_json)
assert response.status_code == BAD_REQUEST, response.data.decode('utf-8')
response = post_json(test_client, simulation_json)
assert response.status_code == client.BAD_REQUEST, response.data.decode('utf-8')
response_json = json.loads(response.data.decode('utf-8'))
message = "'Locataire ou sous-locataire d‘un logement loué vide non-HLM' is not a known value for 'housing_occupancy_status'. Possible values are "
text = dpath.get(response_json, 'households/_/housing_occupancy_status/2017-07')
assert message in text


def test_encoding_entity_name():
def test_encoding_entity_name(test_client):
simulation_json = json.dumps({
"persons": {
"O‘Ryan": {},
Expand All @@ -227,17 +222,17 @@ def test_encoding_entity_name():
})

# No UnicodeDecodeError
response = post_json(simulation_json)
response = post_json(test_client, simulation_json)
response_json = json.loads(response.data.decode('utf-8'))

# In Python 3, there is no encoding issue.
if response.status_code != OK:
if response.status_code != client.OK:
message = "'O‘Ryan' is not a valid ASCII value."
text = response_json['error']
assert message in text


def test_encoding_period_id():
def test_encoding_period_id(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {
Expand Down Expand Up @@ -268,8 +263,8 @@ def test_encoding_period_id():
})

# No UnicodeDecodeError
response = post_json(simulation_json)
assert response.status_code == BAD_REQUEST
response = post_json(test_client, simulation_json)
assert response.status_code == client.BAD_REQUEST
response_json = json.loads(response.data.decode('utf-8'))

# In Python 3, there is no encoding issue.
Expand All @@ -279,17 +274,17 @@ def test_encoding_period_id():
assert message in text


def test_str_variable():
new_couple = deepcopy(couple)
def test_str_variable(test_client):
new_couple = copy.deepcopy(couple)
new_couple['households']['_']['postal_code'] = {'2017-01': None}
simulation_json = json.dumps(new_couple)

response = subject.post('/calculate', data = simulation_json, content_type = 'application/json')
response = test_client.post('/calculate', data = simulation_json, content_type = 'application/json')

assert response.status_code == OK
assert response.status_code == client.OK


def test_periods():
def test_periods(test_client):
simulation_json = json.dumps({
"persons": {
"bill": {}
Expand All @@ -307,8 +302,8 @@ def test_periods():
}
})

response = post_json(simulation_json)
assert response.status_code == OK
response = post_json(test_client, simulation_json)
assert response.status_code == client.OK

response_json = json.loads(response.data.decode('utf-8'))

Expand All @@ -319,7 +314,7 @@ def test_periods():
assert monthly_variable == {'2017-01': 'tenant'}


def test_gracefully_handle_unexpected_errors():
def test_gracefully_handle_unexpected_errors(test_client):
"""
Context
========
Expand Down Expand Up @@ -358,8 +353,8 @@ def test_gracefully_handle_unexpected_errors():
}
})

response = post_json(simulation_json)
assert response.status_code == INTERNAL_SERVER_ERROR
response = post_json(test_client, simulation_json)
assert response.status_code == client.INTERNAL_SERVER_ERROR

error = json.loads(response.data)["error"]
assert f"Unable to compute variable '{variable}' for period {period}" in error
21 changes: 11 additions & 10 deletions tests/web_api/test_entities.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# -*- coding: utf-8 -*-

from http.client import OK
from http import client
import json
import openfisca_country_template
from . import subject

entities_response = subject.get('/entities')
from openfisca_country_template import entities


# /entities


def test_return_code():
assert entities_response.status_code == OK
def test_return_code(test_client):
entities_response = test_client.get('/entities')
assert entities_response.status_code == client.OK


def test_response_data():
entities = json.loads(entities_response.data.decode('utf-8'))
test_documentation = openfisca_country_template.entities.Household.doc.strip()
def test_response_data(test_client):
entities_response = test_client.get('/entities')
entities_dict = json.loads(entities_response.data.decode('utf-8'))
test_documentation = entities.Household.doc.strip()

assert entities['household'] == {
assert entities_dict['household'] == {
'description': 'All the people in a family or group who live together in the same place.',
'documentation': test_documentation,
'plural': 'households',
Expand Down
10 changes: 5 additions & 5 deletions tests/web_api/test_headers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-

from . import distribution, subject
from . import distribution

parameters_response = subject.get('/parameters')


def test_package_name_header():
def test_package_name_header(test_client):
parameters_response = test_client.get('/parameters')
assert parameters_response.headers.get('Country-Package') == distribution.key


def test_package_version_header():
def test_package_version_header(test_client):
parameters_response = test_client.get('/parameters')
assert parameters_response.headers.get('Country-Package-Version') == distribution.version
Loading

0 comments on commit 3123d27

Please sign in to comment.