# Boxes v0.16

In the previous lesson we created a test with some basic behavior we expected, it failed, we fixed the code, and it passed. Nice!

We specifically tested the part in `justify_row`

that handles lines without
any spaces or hyphens. Let's add more tests to see if `justify_row`

does the
right thing. This is just thinking behavior and creating tests for it.

Let's try some very basic behavior we want when there are spaces:

If we feed "aaaaa aaaaa" to `justify_row`

we expect to have the row fully
justified, and the space in the middle to have grown a lot.

30def test_justify_with_spaces():
31 """Test a simple use case with spaces."""
32 row = [boxes.Box(x=i, w=1, h=1, letter="a") for i in range(10)]
33 row[5].letter = " "
34 row[5].stretchy = True
35 page = boxes.Box(w=50, h=50)
36 separation = .1
37
38 assert len(row) == 10
39 assert row[-1].x + row[-1].w == 10
40
41 boxes.justify_row(row, page, separation)
42
43 # Should have the same number of elements
44 assert len(row) == 10
45 # The first element should be flushed-left
46 assert row[0].x == page.x
47 # The last element should be flushed-right
48 # Use approx() because floating point adds a tiny error here
49 assert pytest.approx(row[-1].x + row[-1].w) == page.x + page.w
50 # The element in position 5 must have absorbed all the slack
51 # So is 1 (it's width) + 40 (slack) units wide
52 assert row[5].w == 41

We can tell `pytest`

to run only this specific test:

1$ env PYTHONPATH=. pytest tests/test_justify_row.py::test_justify_with_spaces
2
3============================= test session starts ==============================
4platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
5rootdir: code/lesson5, inifile:
6collected 1 item
7
8tests/test_justify_row.py . [100%]
9
10=========================== 1 passed in 0.24 seconds ===========================

Nice!

How about other behaviors? Our function removes spaces from the right:

66 # Remove all right-margin spaces
67 while row[-1].letter == " ":
68 row.pop()

Let's do a test to make sure we do that!

76def test_justify_trailing_spaces():
77 """Test a use case with traling spaces."""
78 row = [boxes.Box(x=i, w=1, h=1, letter="a") for i in range(10)]
79 row[-1].letter = " "
80 row[-2].letter = " "
81 page = boxes.Box(w=50, h=50)
82 separation = .1
83
84 assert len(row) == 10
85 assert row[-1].x + row[-1].w == 10
86
87 boxes.justify_row(row, page, separation)
88
89 # Should have lost the 2 trailing spaces
90 assert len(row) == 8
91 # The first element should be flushed-left
92 assert row[0].x == page.x
93 # The last element should be flushed-right
94 assert row[-1].x + row[-1].w == page.x + page.w

That works too!

How about lines with a newline character at the end? If we have "aaa aaa aaa\n" then that should not actually be justified, right?

56def test_justify_ends_with_newline():
57 """Test a use case with a newline."""
58 row = [boxes.Box(x=i, w=1, h=1, letter="a") for i in range(10)]
59 row[-1].letter = "\n"
60 page = boxes.Box(w=50, h=50)
61 separation = .1
62
63 assert len(row) == 10
64 assert row[-1].x + row[-1].w == 10
65
66 boxes.justify_row(row, page, separation)
67
68 # Should have the same number of elements
69 assert len(row) == 10
70 # The first element should be flushed-left
71 assert row[0].x == page.x
72 # The last element should NOT be flushed-right
73 assert row[-1].x + row[-1].w == 10

1$ env PYTHONPATH=. pytest tests/test_justify_row.py::test_justify_ends_with_newline
2
3[skipping]
4
5 # The last element should NOT be flushed-right
6> assert row[-1].x + row[-1].w == 10
7E assert (49.0 + 1) == 10
8E + where 49.0 = Box(49.0, 0, 1, 0, "\n").x
9E + and 1 = Box(49.0, 0, 1, 0, "\n").w
10
11tests/test_justify_row.py:68: AssertionError

And it fails. That is not surprising because if you look at our implementation
of `justify_row()`

it doesn't check for newlines at all! That is because we
are handling that case in `layout()`

instead. This is arguably correct. On the
other hand, if we want to encapsulate the justification of a row in this
function it makes sense to move that here.

At this point, however, that is not implemented but it's not really a bug
because it *is* handled. So, we can mark this test as an "expected failure"
and put a pin in it so we can come back to it. We mark it as expected failure
using the `pytest.mark.xfail`

decorator, and add a `FIXME`

comment to remember
to come back.

55@pytest.mark.xfail # FIXME: justify doesn't handle newlines yet!
56def test_justify_ends_with_newline():
57 """Test a use case with a newline."""

The other thing this function does is put everything in an actual row considering "separation":

82 # And we put each thing next to the previous one
83 for j, b in enumerate(row[1:], 1):
84 b.x = row[j - 1].x + row[j - 1].w + separation

Let's test if it does indeed do that!

97def test_justify_puts_things_in_a_row():
98 """Test a simple use case with spaces."""
99 row = [boxes.Box(x=i, w=1, h=1, letter="a") for i in range(10)]
100 row[5].letter = " "
101 row[5].stretchy = True
102 page = boxes.Box(w=50, h=50)
103 separation = .1
104
105 assert len(row) == 10
106 assert row[-1].x + row[-1].w == 10
107
108 boxes.justify_row(row, page, separation)
109
110 # Should have the same number of elements
111 assert len(row) == 10
112 # The first element should be flushed-left
113 assert row[0].x == page.x
114 # The last element should be flushed-right
115 # Use approx() because floating point adds a tiny error here
116 assert pytest.approx(row[-1].x + row[-1].w) == page.x + page.w
117 # All elements should be separated correctly.
118 separations = [
119 separation - (row[i].x - (row[i - 1].x + row[i - 1].w))
120 for i in range(1, len(row))
121 ]
122 # Again, floating point is inaccurate
123 assert max(separations) < 0.00001
124 assert min(separations) > -0.00001

And we finish running our whole test suite and see what happens.

1$ env PYTHONPATH=. pytest
2============================= test session starts ==============================
3platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
4rootdir: part2/code/lesson5, inifile:
5collected 5 items
6
7tests/test_justify_row.py ..x.. [100%]
8
9===================== 4 passed, 1 xfailed in 0.37 seconds ======================

Notice how we have 4 passing tests and an `xfail`

.

What we have been working on is called "test coverage". We now have tests that
cover the expected behavior of `justify_row`

. That means that when, in the
next lesson, we **change** that behavior, we can be fairly sure we **know**
how we are changing it.

- Full source code for this lesson boxes.py
- Difference with code from last lesson