Testing the export function

In the same test module, I have defined another class that represents a test suite for the export function. Here it is:

# tests/test_api.py
class TestExport:

@pytest.fixture
def csv_file(self, tmpdir):
yield tmpdir.join("out.csv")

@pytest.fixture
def existing_file(self, tmpdir):
existing = tmpdir.join('existing.csv')
existing.write('Please leave me alone...')
yield existing

Let's start understanding the fixtures. We have defined them at class-level this time, which means they will be alive only for as long as the tests in the class are running. We don't need these fixtures outside of this class, so it doesn't make sense to declare them at a module level like we've done with the user ones.

So, we need two files. If you recall what I wrote at the beginning of this chapter, when it comes to interaction with databases, disks, networks, and so on, we should mock everything out. However, when possible, I prefer to use a different technique. In this case, I will employ temporary folders, which will be born within the fixture, and die within it, leaving no trace of their existence. I am much happier if I can avoid mocking. Mocking is amazing, but it can be tricky, and a source of bugs, unless it's done correctly.

Now, the first fixture, csv_file, defines a managed context in which we obtain a reference to a temporary folder. We can consider the logic up to and including the yield, as the setup phase. The fixture itself, in terms of data, is represented by the temporary filename. The file itself is not present yet. When a test runs, the fixture is created, and at the end of the test, the rest of the fixture code (the one after yield, if any) is executed. That part can be considered the teardown phase. In this case, it consists of exiting the context manager, which means the temporary folder is deleted (along with all its content). You can put much more in each phase of any fixture, and with experience, I'm sure you'll master the art of doing setup and teardown this way. It actually comes very naturally quite quickly.

The second fixture is very similar to the first one, but we'll use it to test that we can prevent overwriting when we call export with overwrite=False. So we create a file in the temporary folder, and we put some content into it, just to have the means to verify it hasn't been touched.

Notice how both fixtures are returning the filename with the full path information, to make sure we actually use the temporary folder in our code. Let's now see the tests:

# tests/test_api.py
def test_export(self, users, csv_file):
export(csv_file, users)

lines = csv_file.readlines()

assert [
'email,name,age,role ',
'[email protected],Primus Minimus,18, ',
'[email protected],Maximus Plenus,65,emperor ',
] == lines

This test employs the users and csv_file fixtures, and immediately calls export with them. We expect that a file has been created, and populated with the two valid users we have (remember the list contains three users, but one is invalid).

To verify that, we open the temporary file, and collect all its lines into a list. We then compare the content of the file with a list of the lines that we expect to be in it. Notice we only put the header, and the two valid users, in the correct order.

Now we need another test, to make sure that if there is a comma in one of the values, our CSV is still generated correctly. Being a comma-separated values (CSV) file, we need to make sure that a comma in the data doesn't break things up:

# tests/test_api.py
def test_export_quoting(self, min_user, csv_file):
min_user['name'] = 'A name, with a comma'

export(csv_file, [min_user])

lines = csv_file.readlines()
assert [
'email,name,age,role ',
'[email protected],"A name, with a comma",18, ',
] == lines

This time, we don't need the whole users list, we just need one as we're testing a specific thing, and we have the previous test to make sure we're generating the file correctly with all the users. Remember, always try to minimize the work you do within a test.

So, we use min_user, and put a nice comma in its name. We then repeat the procedure, which is very similar to that of the previous test, and finally we make sure that the name is put in the CSV file surrounded by double quotes. This is enough for any good CSV parser to understand that they don't have to break on the comma inside the double quotes.

Now I want one more test, which needs to check that whether the file exists and we don't want to override it, our code won't touch it:

# tests/test_api.py
def test_does_not_overwrite(self, users, existing_file):
with pytest.raises(IOError) as err:
export(existing_file, users, overwrite=False)

assert err.match(
r"'{}' already exists.".format(existing_file)
)

# let's also verify the file is still intact
assert existing_file.read() == 'Please leave me alone...'

This is a beautiful test, because it allows me to show you how you can tell pytest that you expect a function call to raise an exception. We do it in the context manager given to us by pytest.raises, to which we feed the exception we expect from the call we make inside the body of that context manager. If the exception is not raised, the test will fail.

I like to be thorough in my test, so I don't want to stop there. I also assert on the message, by using the convenient err.match helper (watch out, it takes a regular expression, not a simple string–we'll see regular expressions).

Finally, let's make sure that the file still contains its original content (which is why I created the existing_file fixture) by opening it, and comparing all of its content to the string it should be.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.119.199