9th Online Meeting, August 31, 2020
Mentor: Petr Viktorin
00hr:18mins
Petr started to reorganize tests on a separate branch because it was not to be done in one shot. In order not to lose the work done he commited it as WIP (work in progress) without pushing it.
To continue working on it he switched to the reorganize tests
branch.
Each commit in Git represents the state of the whole project. Git show
and gitk --all
both display all the changes since the last commit. In the background however it actually saves the whole content/state of each commit. Saving the whole state of all commits makes it possible for Git to display all that happened within each commit. In reality, it compares the state of the previous/parent commit, and the state of the current commit by listing the diff --git a/
of all files from both commits.
To do the git rebase master
, Git takes the diff or all changes from the commit and attempts to apply the same changes to the current master. More precisely, Git creates a new commit, goes file by file and tries to apply all changes until a conflict appears or until all changes are applied to the master.
00hr:21min
The "modify/delete" means that test_demo.py
changed in the master and the same file was deleted from the reorganize-tests branch which is being rebased.
"Version HEAD of test_demo.py left in tree" means that the file test_demo.py
was left in the working tree although it was deleted/renamed into test_expected_output.py
in the reorganize-tests branch. This change was done because test_demo.py contained repetitive tests which import an app, freeze the same and than check if the app does what it was supposed to do.
Future Issue: 00hr:24min
The new tests do not assert test_flask_url_for_custom_prefix_without_port(tmp_path)
.
After all conflicts are resolved and the changes are commited just git rebase --continue
to complete the rebase. Finally, reorganize-tests is established on the master branch.
00hr:27min
import importlib
from freezeyt import freeze
def test_output(tmp_path, module name='demo app'):
module = importlib.import_module(module_name) #naimportuje si applikace ze importlib
app = module.app
freeze(app, tmp_path)
expected = Path(__file__).parent / 'fixtures' / module_name
print(expected)
cmp = filecmp.dircmp(tmp_path, expected, ignore=[]) #comparison
assert False
At this point this test freezes an app from the demo_app module. Creates a variable expected
(the path to the fixtures folder) in order to be able to compare it's content to the actual output of the freeze. We used filecmp to make the comparison between what we froze and what is in the expected fixtures folder. Next it is needed to create the asserts for this test.
print(cmp.left_only) #files found only in the left folder (in this case tmp_path)
print(cmp.right_only) #files found only in the right folder (in this case expected)
print(cmp.diff_files)
print(cmp.common_funny) #in case the same thing is a file on one side and a folder or smth. else on the other side
print(cmp.subdirs) #comparison of dictionaries {directory : content}
print(cmp.diff_files) #different files on each side
It is better to create an assert_dirs_same function which will do the comparison.
00hr:38min
Created folders same
and testdir
, inside the fixtures folder. testdir
contains a file.txt with some random text, and a sub-folder with a file inside it. Inside the fixtures folder is a dirs_same
folder containing all possible variations of testing folders which should cause the test to fail like missing_file
, extra_file
, extra_in_subdir
, missing_dir
, missing_file_in_dir
, same
, funny
, diff_files
.
At this stage test_assert_dirs_same
compares all these possible variations to both testdir
and same
since these two need to be identical for the tests to pass:
def test_assert_dirs_same():
fixture_path = Path(__file__).parent / 'fixtures' 'dirs_same'
for path in fixture_path.iterdir():
if path.name in ('testdir', 'same'):
assert_dirs_same(path, fixture_path / 'testdir')
else:
with pytest.raises(AssertionError):
assert_dirs_same(path, fixture_path / 'testdir')
Everytime test_assert_dirs_same
fails we solve each failure and build the assert_dirs_same
gradually. The first one that failed was funny so the appropriate assertion was added to assert_dirs_same
:
if cmp.common_funny:
raise AssertionError(f'Funny dofferemces: {cmp.common_funny}')
The same is done until all test_assert_dirs_same
conditions pass without a failure.
00hr:54min
The test does not count on subdirectories so we need to find a way for each subdir
to pass the test_assert_dirs_same
as well. The best way to do this is to divide assert_dirs_same
in two functions where one will be calling the other.
In this case, cmp
notes all files and folders that are within each folder using filecmp.dircmp
and does the same again for each subfolder every time ```assert_dirs_same``` calls ```assert_cmp_same.
01hr:09min
Improve the description to AssertionErrors in assert_cmp_same gradually as further test failures appear. It would be good to get the absolute path to the folder where the failure happens for example.
When calling assert_dirs_same inside test_output we got an AssertionError: Files does not have expected content: ['index.html']
The following asks pytest to compare the expected and the output file to make it easier to locate the the difference:
if cmp.diff_files:
for filename in cmp.diff_files:
path1 = Path(cmp.left) / filename
path2 = Path(cmp.right) / filename
assert parh1.read_text() == path2.read_text()
This provided the following:
01hr:19min
Interactive rebase was used to merge the work_in_progress commit to reorganize tests. $ git rebase --interactive <branch_from_where_to_start>
This way you get a list of a few commands to change your commits history:
Reword lets you change the description of a commit and we squashed (melded) the two commits into one.
01hr:25min
$ git --reflog
Shows the entire and complete history of commits which existed at any point in time, and makes it possible to retrieve lost commits:
This history can be browsed using the commit name: $ git show HEAD @ {3}
- where the history was three changes ago, or $ git show HEAD@{3.weeks.ago}
. This way you can return to any of the changes at any point in he commit history. Also available $ git log HEAD@{3.weeks.ago}
and $ gitk --all HEAD@{3.weeks.ago}
01hr:33min
What we did so far compares the current frozen output to the expected output and we are supposed to fix the differences manually. We want the corrected output to be generated by a special mode. When we run the tests in this special mode the expected output is created on its own. How to ensure that tests are running in a special mode? How to pass some information to a running application?
Two ways
Create a switch $ python -m pytest -vv --create-data
. This switch triggers pytest to create the test data/expected output
$ export TEST_CREATE_EXPECTED_OUTPUT=1
, and than check if this environmental variable is not already set in the test. If yes, no need to compare, just copy the content of the frozen into the expected using a special module shutil:
```python
if 'TEST_CREATE_EXPECTED_OUTPUT' in os.environ:
shutil.copytree(tmp_path, expected)
```
From this point on, when adding a new tests we have to run them from 'TEST_CREATE_EXPECTED_OUTPUT' and afterwards check if the generated files contain the expected output.
01hr:47min
So far the tests run on one app only, and to test the rest of the demo_apps we could use the for cycle used in the test_assert_dirs_same()
. However using a loop has a disadvantage in because it breaks a the first exception.
This is why @pytest.mark.parametrize: parametrizing test functions was used.
01hr:50min
The function takes the dir_names
argument, add it to the path, and than use @pytest.mark.parametrize('dir_names', ['difference', 'extra'])
setting the dir_names
argument to 'difference' and 'extra'. Making two tests out of a single function. More precisely, a single test function receives and tests two different parameters, 'difference' and 'extra'.
test_assert_dirs_same was changed accordingly, the fixture_path was made into a global variable named DIRS_SAME_FIXTURES and than we are able to iterate over it inside the test, which made it possible to test all of the apps from inside fixtures with this single test: