Testing

The test cases are located in fontParts.test.test_*.

Test Structure

import unittest
from fontParts.base import FontPartsError

class TestFoo(unittest.TestObject):

    # --------------
    # Section Header
    # --------------

    def getFoo_generic(self):
      # code for building the object

    def test_bar(self):
        # Code for testing the bar attribute.

    def test_changeSomething(self):
        # Code for testing the changeSomething method.

Test Definitions

The test definitions should be developed by following the FontParts API documentation. These break down into two categories.

  1. attributes

  2. methods

These will be covered in detail below. In general follow these guidelines when developing

  1. Keep the test focused on what is relevant to what is being tested. Don’t test file saving within an attribute test in a sub-sub-sub-sub object.

  2. Make the tests as atomic as possible. Don’t modify lots of parts of an object during a single test. That makes the tests very hard to debug.

  3. Keep the code clear and concise so that it is easy to see what is being tested. Add documentation to clarify anything that is ambiguous. Try to imagine someone trying to debug a failure of this test five years from now. Will they be able to tell what is going on in the code?

  4. If testing an edge case, make notes defining where this situation is happening, why it is important and so on. Edge case tests often are hyper-specific to one version of one environment and thus have a limited lifespan. This needs to be made clear for future reference.

  5. Test valid and invalid input. The base implementation’s normalizers define what is valid and invalid. Use this as a reference.

  6. Only test one thing per test case. Tests are not a place to avoid repeated code, it’s much easier to debug an error in a test when that test is only doing one thing.

Testing Attributes

Attribute testing uses the method name structure test_attributeName. If more than one method is needed due to length or complexity, the additional methods use the name structure test_attributeNameDescriptionOfWhatThisTests.

def test_bar_get(self):
    foo, unrequested = self.getFoo_generic()
    # get
    self.assertEqual(
        foo.bar,
        "barbarbar"
    )

def test_bar_set_valid(self):
    foo, unrequested = self.getFoo_generic()
    # set: valid data
    foo.bar = "heyheyhey"
    self.assertEqual(
        foo.bar,
        "heyheyhey"
    )

def test_bar_set_invalid(self):
    foo, unrequested = self.getFoo_generic()
    # set: invalid data
    with self.assertRaises(FontPartsError):
        foo.bar = 123

def test_barSettingNoneShouldFail(self):
    foo, unrequested = self.getFoo_barNontShouldFail()
    with self.assertRaises(FontPartsError):
        foo.bar = None

Getting

When testing getting an attribute, test the following:

  • All valid return data types. Use the case definitions to specify these.

  • (How should invalid types be handled? Is that completely the responsibility of the environment?)

Setting

When testing setting an attribute, test the following:

  • All valid input data types. For example if setting accepts a number, test int and float. If pos/neg values are allowed, test both.

  • A representative sample of invalid data types/values.

If an attribute does not support setting, it should be tested to make sure that an attempt to set raises the appropriate error.

Testing Methods

Testing methods should be done atomically, modifying a single argument at a time. For example, if a method takes x and y arguments, test each of these as independently as possible. The following should be tested for each argument:

  • All valid input data types. For example if setting accepts a number, test int and float. If pos/neg values are allowed, test both.

  • A representative sample of invalid data types/values.

def test_changeSomething(self):
    bar, unrequested = self.getBar_something()
    bar.changeSomething(x=100, y=100)
    self.assertEqual(
        bar.thing,
        (100, 100)
    )

def test_changeSomething_invalid_x(self):
    bar, unrequested = self.getBar_something()
    with self.assertRaises(FontPartsError):
       bar.changeSomething(x=None, y=100)

def test_changeSomething_invalid_y(self):
    bar, unrequested = self.getBar_something()
    with self.assertRaises(FontPartsError):
       bar.changeSomething(x=100, y=None)

Objects for Testing

Objects for testing are defined in methods with the name structure getFoo_description. The base object will be generated by the environment by calling self.objectGenerator("classIdentifier"). This will return a fontParts wrapped object ready for population and testing. It will also return a list of objects that were/are required for generating/retaining the requested object. For example, if an environment doesn’t support orphan glyphs, the unrequested list may contain a parent font. The objects in the unrequested list must not be used within tests.

def getFoo_generic(self):
  foo = self.objectGenerator("foo")
  foo.bar = "barbarbar"
  return foo, []

To Do

  • Establish tests for pen protocol in test_glyph.