Should I assert the same values in every test? - python

I have simple class with the public build method I want to test. Currently I assert all values it returns in every test. Is it a good practice or I should write one test for static values and in other tests only check values which change depending on input?
Implementation
class FiltersAttachment:
TYPE_OPTIONS = [
{"text": "All types", "value": "all"},
{"text": ":link: Webpages", "value": "web_pages"}
]
STATUS_OPTIONS = [
{"text": "Available / Unavailable", "value": "all"},
{"text": ":white_circle: Available", "value": "available"},
{"text": ":red_circle: Unavailable", "value": "unavailable"}
]
#classmethod
def _filter_options(cls, options, selected):
return list(filter(lambda t: t['value'] == selected, options))
#classmethod
def build(cls, check_type='', status=''):
return {
'fallback': 'Filters',
'callback_id': 'resource_filters',
'color': '#d2dde1',
'mrkdwn_in': ['text'],
'actions': [
{
'name': 'resource_type',
'text': 'Type',
'type': 'select',
'options': cls.TYPE_OPTIONS,
'selected_options': cls._filter_options(
cls.TYPE_OPTIONS, check_type)
},
{
'name': 'resource_status',
'text': 'Status',
'type': 'select',
'options': cls.STATUS_OPTIONS,
'selected_options': cls._filter_options(
cls.STATUS_OPTIONS, status)
}
]
}
Tests
class TestFiltersAttachment(TestCase):
def assert_attachment(self, attachment):
self.assertEqual(attachment['fallback'], 'Filters')
self.assertEqual(attachment['callback_id'], 'resource_filters')
self.assertEqual(attachment['color'], '#d2dde1')
self.assertEqual(attachment['mrkdwn_in'], ['text'])
type_action = attachment['actions'][0]
self.assertEqual(type_action['name'], 'resource_type')
self.assertEqual(type_action['text'], 'Type')
self.assertEqual(type_action['type'], 'select')
self.assertEqual(type_action['options'][0]['text'], 'All types')
self.assertEqual(type_action['options'][0]['value'], 'all')
self.assertEqual(type_action['options'][1]['text'], ':link: Webpages')
self.assertEqual(type_action['options'][1]['value'], 'web_pages')
status_action = attachment['actions'][1]
self.assertEqual(status_action['name'], 'resource_status')
self.assertEqual(status_action['text'], 'Status')
self.assertEqual(status_action['type'], 'select')
self.assertEqual(status_action['options'][0]['text'], 'Available / Unavailable')
self.assertEqual(status_action['options'][0]['value'], 'all')
self.assertEqual(status_action['options'][1]['text'], ':white_circle: Available')
self.assertEqual(status_action['options'][1]['value'], 'available')
self.assertEqual(status_action['options'][2]['text'], ':red_circle: Unavailable')
self.assertEqual(status_action['options'][2]['value'], 'unavailable')
def test_all_type_selected(self):
attachment = FiltersAttachment.build(check_type='all')
self.assert_attachment(attachment)
selected_type = attachment['actions'][0]['selected_options'][0]
self.assertEqual(selected_type['text'], 'All types')
self.assertEqual(selected_type['value'], 'all')
def test_all_status_selected(self):
attachment = FiltersAttachment.build(status='all')
self.assert_attachment(attachment)
selected_status = attachment['actions'][1]['selected_options'][0]
self.assertEqual(selected_status['text'], 'Available / Unavailable')
self.assertEqual(selected_status['value'], 'all')
...

One of the criteria for the quality of a test suite is, how well the test suite supports you in case of test failures to identify the problem. Ideally, you should be able to identify the problem alone by looking at which tests failed and which did not. You should not need to use a debugger to find out what actually went wrong.
The way you have written your tests will not give you the best possible support. You have packed many assertions in one test function. Therefore, the test functions will fail for many different reasons, and when you see one of the functions fail, you will have to do a detailed analysis or use debugging to find out for which reason it failed. When making your tests check aspects redundantly (as you have asked in your question), you make them even less specific, which makes the problem worse.
Therefore, each test should check one specific aspect, such that a failure of a test gives the most specific information. This is achieved by the combination of the following two principles:
Each test should verify one specific aspect.
There should not be redundant tests for the same aspect.
Turning each assertion into a test of its own can be done conveniently with the help of so called parameterized tests. Some hints for Python can be found at this question: How do you generate dynamic (parameterized) unit tests in python?

Related

Use 'pytest' to mock a function that can return multiple exceptions

Using pytest, I wish to mock a function that can raise several exceptions. My app will catch the exception and create a response object, and I need to assert that each response contains the correct message, type, and in reality several other properties.
In the first instance, I have created a separate fixture to mock the function and raise each exception, and then I'm passing those fixtures in to a test with a series of events. However, because my fixtures are mocking the same function, the exception raised for every test is the same - in this case, that would be mock_exc_2, the last fixture passed to the test.
One thing I know will work is to separate the test function into multiple functions, but that seems inefficient because any future change would need to be made to multiple functions.
What is the most appropriate / efficient way to do this with with pytest?
Fixtures in 'conftest.py'
#pytest.fixture(scope='function')
def mock_exc_1(mocker):
def mock_response(self, path):
raise MissingOrgIdException()
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
#pytest.fixture(scope='function')
def mock_exc_2(mocker):
def mock_response(self, path):
# Parameter values are not emitted in the error message that is included in the response to the user.
raise InvalidOrgIdException('xxx', 'xxx')
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
# Working fixtures for 'event and 'mock_context' go here.
Broken tests in 'test_events.py'
In this scenario, only the last test is successful because both mock_exc_1 and mock_exc_2 are mocking the same function.
bad_request_args = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_context, mock_exc_1, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
Working tests in 'test_events.py'
Here the tests will pass, because each test is only using the fixture that raises the correct exception for the mocked function.
However, in reality there are more than two exceptions to test, and having to maintain a parameter with the parametrize args and a function to test each exception seems inefficient and prone to error when a change is made.
bad_request_args_1 = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
)
]
)
bad_request_args_2 = ('event, expected',
[
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args_1, indirect=['event'])
def test_400_events_1(event, expected, mock_context, mock_exc_1):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
#pytest.mark.parametrize(*bad_request_args_2, indirect=['event'])
def test_400_events_2(event, expected, mock_context, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
It seems that at the moment there is no "proper" way to do this. However, it is possible to do this by using the request.getfixturevalue('fixture')
bad_request_args = ('event, expected, mock_fixture_name',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
},
'mock_exc_1'
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
},
'mock_exc_2'
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_fixture_name, mock_context, request):
request.getfixturevalue(mock_fixture_name)
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']

allowing the user to input two parameters

i want to allow the user to be able to type {misc} of {nation} and specific information will be displayed based on the input. i have tired several different ways but seem to never find a way to make it work. any help is appreciated thanks!
(Sorry about title didnt really know what to title it)
def part1():
txt = "{} of {}"
info = input('''What do you want to know?: ''')
if info == "{} of {}":
print(txt.format(misc.get(info), nations.get(info)))
else:
print("i dont know what your talking about")
nations = {
'fiji': {
'location': 'Oceana',
'capital_city': 'Suva'},
'france': {
'location': 'Europe',
'capital_city': 'Paris'},
}
misc = {'population': {'fiji': '847,706',
'france': '60,495,540'},
'location': {'fiji': 'Oceana',
'france': 'Europe'},
}
part1()
Not sure exactly what you're trying to do, but there are a few issues with your code, try something like this:
def part1():
txt = "{} of {}"
info = input('''What do you want to know?: ''')
if ' of ' in info:
params = info.split(' of ')
else:
print("i dont know what your talking about")
return
print(params[0])
print(misc.get(params[0]).get(params[1]))
nations = {
'fiji': {
'location': 'Oceana',
'capital_city': 'Suva'},
'france': {
'location': 'Europe',
'capital_city': 'Paris'},
}
misc = {'population': {'fiji': '847,706',
'france': '60,495,540'},
'location': {'fiji': 'Oceana',
'france': 'Europe'},
}
part1()
Currently with this code there isnt really a way to know which object you're targeting (nations or misc). To remedy this I would ask the user an initial question to determine which type of info they want.
Additional improvements could be checking the data you're getting from the dictionaries. In the case you get nothing from the dictionaries (.get returns None) you could notify the user the info you have available).
A quick and dirty way to do this if you are not using much data, is to just put the info directly into the if statements and call the function. For instance:
def part1():
info = input("What do you want to know?: ")
if info == "location of fiji":
print("Oceana:")
elif info == "capital_city of fiji":
print("Suva")
elif info == "location of France":
print("Europe")
elif info == "capital_city of France":
print("Paris")
else:
print("i dont know what your talking about")
part1()
If you are using a lot of data points, then it is probably better to have a list/database like you have though. Here's the quick and dirty fix if you want it though.
(Also, if you use this method, you can convert the user input string into all lowercase using .lower I believe. This way, capitalization does not matter when inputting).
A good way to do this would be if else/elif statements that way the program can check for multiple inputs rather then trying to check for 2 different inputs at once.
Viktor Basharkevich has pretty much the right idea on what to do
You could also try a split method
input().split(x, y)
x, y = input("What do you want to know?").split()
This might work
Or you could try a list

Analysis of fields in nested document elasticsearch

I am using a document with nested structure in it where the content is analysed in spite of my telling it "not analysed". The document is defined as follows:
class SearchDocument(es.DocType)
# Verblijfsobject specific data
gebruiksdoel_omschrijving = es.String(index='not_analyzed')
oppervlakte = es.Integer()
bouwblok = es.String(index='not_analyzed')
gebruik = es.String(index='not_analyzed')
panden = es.String(index='not_analyzed')
sbi_codes = es.Nested({
'properties': {
'sbi_code': es.String(index='not_analyzed'),
'hcat': es.String(index='not_analyzed'),
'scat': es.String(index='not_analyzed'),
'hoofdcategorie': es.String(fields= {'raw': es.String(in dex='not_analyzed')}),
'subcategorie': es.String(fields={'raw':es.String(index='not_analyzed')}),
'sub_sub_categorie': es.String(fields= {'raw': es.String(index='not_analyzed')}),
'bedrijfsnaam': es.String(fields= {'raw': es.String(index='not_analyzed')}),
'vestigingsnummer': es.String(index='not_analyzed')
}
})
As is clear, it says "not analysed" in the document for most fields. This works OK for the "regular fields". The problem is in the nested structure. There the hoofdcategorie and other fields are indexed for their separate words instead of the unanalysed version.
The structure is filled with the following data:
[
{
"sbi_code": "74103",
"sub_sub_categorie": "Interieur- en ruimtelijk ontwerp",
"vestigingsnummer": "000000002216",
"bedrijfsnaam": "Flippie Tests",
"subcategorie": "design",
"scat": "22279_12_22254_11",
"hoofdcategorie": "zakelijke dienstverlening",
"hcat": "22279_12"
},
{
"sbi_code": "9003",
"sub_sub_categorie": "Schrijven en overige scheppende kunsten",
"vestigingsnummer": "000000002216",
"bedrijfsnaam": "Flippie Tests",
"subcategorie": "kunst",
"scat": "22281_12_22259_11",
"hoofdcategorie": "cultuur, sport, recreatie",
"hcat": "22281_12"
}
]
Now when I retrieve aggregates it has split the hoofdcategorie in 3 different words ("cultuur", "sport", "recreatie"). This is not what I want, but as far as I know I have specified it correctly using the "not analysed" phrase.
Anyone any ideas?

Attendees in Google Calendar not always in the same order

So I've just started using the google calendar api and I've had good results so far. I add attendees with their name and email in the events dictionary, like so
events = {
# other stuff here and then this
'items': [
# lots of stuff here, followed by
'attendees': [
{
'email': email1,
'displayName': name1
},
{
'email': email2,
'displayName': name2
},
],
###
]
}
Adding them goes fine, but when I access them, I'm never guaranteed of their order. I thought I could just access the emails like this
for event in events['items']:
print "email1 = " + str(event['attendees'][0]['email'])
print "email2 = " + str(event['attendees'][1]['email'])
and I can. And I've learned that lists in python always have their order preserved, which is convenient because I wanted to access the dictionaries inside the list with the index of the list. But what I've learned is that sometimes the 0 index refers to email1 and sometimes it refers to email2. Why the inconsistency? Is it inherent to the google calendar api or is there something about having dictionary objects within a python list that relaxes the order preservation assumption? Or is it something else I'm missing?
So, as #Colonel Thirty Two pointed out, while lists preserve order, how google return data into a list may not be in the same order as it was submitted to them. This order inconsistency with attendees is inconvenient if you are wanting to count on that order for the retrieval of attendees with something like
for event in events['items']:
print "email1 = " + str(event['attendees'][0]['email'])
print "email2 = " + str(event['attendees'][1]['email'])
What's more is that very few fields are writable with the google calendar api. What is writable, however, is comments. So, I added a value to that field to make the attendees identifiable. Like so
'attendees': [
{
'email': agent_email,
'displayName': agent_name,
'comment': 'agent'
},
{
'email': event_attendee_email,
'displayName': event_attendee_name,
'comment': 'client'
},
Using comment as an identifier helped me in retrieving the email and displayName of each attendee with a simple if-statement.
for i in range(len(event['attendees'])):
if event['attendees'][i]['comment'] == 'client':
event['attendees'][i]['displayName'] = event_attendee_name
event['attendees'][i]['email'] = event_attendee_email
Now it doesn't matter that the google calendar api submits my attendees back to me in a different order than the one in which I added them. I can now retrieve the attendees so I can change them. Problem solved.

Python doit - Use arguments in dependent tasks

I have 2 doit tasks, one having a dependency on the other. For example:
def task_deploy():
return {
'actions': ['do some deploy commands'],
'file_dep': ['dist'],
'params': [{'name': 'projectName',
'short': 'p',
'long': 'projectName',
'default': 'project',
'type': str,
'help': 'The project name to deploy.'}]
}
def task_create_distibution_archive():
return {
'actions': ['do something that requires projectName'],
'doc': 'Creates a zip archive of the application in "dist"',
'targets': ['dist']
}
Is there a way to share or pass the arguments of a task to another one? I have read pretty much everything I could on task creation and dependency on pydoit.org, but haven't found anything similar to what I want.
I am aware that I could use yield to create these two tasks at the same time, but I'd like to use a parameter when executing the task, not when I am creating it.
Is there a way to share or pass the arguments of a task to another one?
Yes. Using getargs: http://pydoit.org/dependencies.html#getargs
In your example, you would need to add another action to the task deploy just to save the passed parameter.
You could just use a global variable like commonCommand. If you have more complex needs, create a class to handle it.
class ComplexCommonParams(object):
def __init__(self):
self.command = 'echo'
params = ComplexCommonParams()
commonCommand='echo'
def task_x():
global commonCommand
return {
'actions': [ commonCommand + ' Hello2 > asdf' ],
'targets': ['asdf']
}
def task_y():
global commonCommand
return {
'actions': [ commonCommand+' World' ],
'file_dep': ['asdf'],
'verbosity':2}

Categories

Resources