Asset Management¶
There are two times when tests need access to files on disk
the function under test requires a file on disk (usually by filename)
a function requires a large amount of data, specially formatted data, or the data is more easily stored as a file on disk rather than being embedded in the test itself. E.g.:
- XML
- JSON
- Images
- INI/Configuration files
- A Python script
- etc.
Granite supplies the AssetMixin
to provide support for easily accessing test asset files.
Note
If you need to also write files to disk, check out Temporary Projects.
Setup¶
To setup, add the AssetMixin
to the list of base classes on your TestCase class and create a
class level attribute ASSET_DIR
that points to the directory containing your asset files:
# tests/some_test.py
import os
from granite.testcase import TestCase, AssetMixin
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
class MyTestCase(AssetMixin, TestCase):
# assume that the asset directory exists at `tests/assets`
ASSET_DIR = os.path.join(THIS_DIR, 'assets')
Note
The AssetMixin
comes before the concrete TestCase
class.
Getting Asset Filenames¶
Use get_asset_filename
to get the absolute path to a filename within the ASSET_DIR
.
For example:
# tests/some_test.py
def test_that_foo_can_read(self):
# assume that `tests/assets/some_file.txt` exists
filename = self.get_asset_filename('some_file.txt')
# pass the absolute path to the foo() function
foo(filename)
The path parameter acts just like os.path.join()
and can accept multiple parameters to
be joined. For example:
>>> self.get_asset_filename('path', 'to', 'my', 'file.txt')
'/absolute/path/to/my/file.txt'
If the given path does not exist, an AssetNotFound
error will be raised:
>>> self.get_asset_filename('path/to/some/nonexistent/file.txt')
Traceback
...
AssetNotFound: self.get_asset_filename() was called with ...
Reading from an Asset File¶
Use read_asset_file
to open the asset file and return its contents:
# tests/some_test.py
def test_that_xml_can_be_parsed(self):
xml = self.read_asset_file('my.xml')
root = foo(xml)
self.assertEqual(etree.tostring(root), xml)
Under the hood, read_asset_file()
uses get_asset_filename()
so path also accepts multiple arguments
and will os.path.join()
all of them together to form a single path.
Additionally, use the mode
keyword argument to specify how the file should be opened. For example,
your function under test requires an image file that needs to be opened in binary mode:
# tests/some_test.py
def test_that_image_size_is_returned(self):
img = self.read_asset_file('1920x1080.jpg', mode='rb')
size = foo(img)
self.assertEqual((1920, 1080), size)
Advanced setup¶
If you find that all (or most) of your tests require access to the asset directory, add the AssetMixin
to your test’s BaseTestCase
class:
# tests/__init__.py
from granite.testcase import TestCase, AssetMixin
class BaseTestCase(AssetMixin, TestCase):
# assume that `tests/assets` exists
ASSET_DIR = os.path.join(THIS_DIR, 'assets')
Then, simply inherit from your BaseTestCase
in your child TestCase
classes to get the asset
functionality:
# tests/some_test.py
from tests import BaseTestCase
class TestSomething(BaseTestCase):
# self.get_asset_filename() and self.read_asset_file() exist!
Using different directories per TestCase¶
One test may require one directory, and another test may use another. Simply change the ASSET_DIR
to use a different directory for the specific TestCase instance:
# tests/some_test.py
from tests import BaseTestCase
class TestSomething(BaseTestCase):
ASSET_DIR = os.path.join(THIS_DIR, 'other', 'assets')
Better asset organization¶
If your tests require a lot of asset files it’s a good idea to try and organize files that are specific to
some tests into their own directory. For example, test_foo.py
requires files a.txt
and b.txt
while test_bar.py
requires c.txt
and d.txt
. The resulting file structure is suggested:
tests/
\ assets
\ foo
| | - a.txt
| | - b.txt
\ bar
| - c.txt
| - d.txt
However, this requires every use of get_asset_filename()
or read_asset_file()
to require the
directory prefix (either foo
or bar
). Instead, the BaseTestCase.ASSET_DIR
attribute can
be extended:
# tests/test_foo.py
import os
from tests import BaseTestCase
class TestFoo(BaseTestCase):
ASSET_DIR = os.path.join(BaseTestCase.ASSET_DIR, 'foo')
This way, if the asset directory is ever moved, the BaseTestCase class will be the only place that needs to be updated.