Closed ashfall closed 7 years ago
@markrwilliams Do you have thoughts about how to add the non-default length case test? I feel like the whole point of using fixtures is to avoid having to repeat writing similar tests with different sets of inputs, and yet I keep defaulting to creating a new test case for the non-default case and repeating code. How would you do it?
@ashfall I spent some time thinking about this. I'm no py.test expert, and I don't think I have a great answer, but I'll share what I know.
Let's consider a test like the one you linked to:
import pytest
import construct
@pytest.mark.parametrize(
'obj,serialized',
[(1, '\x01')],
)
class TestSerialization(object):
@pytest.fixture
def con(self):
return construct.UBInt8("A")
def test(self, con, obj, serialized):
assert con.build(obj) == serialized
We can't do this:
import pytest
import construct
@pytest.fixture
def con():
return construct.UBInt8("A")
@pytest.mark.parametrize(
'obj,serialized,con',
[(1, '\x01', con)],
)
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
Because con
is a py.test fixture, not an actual object.
We don't want to do this:
import pytest
import construct
@pytest.fixture
def con():
return construct.UBInt8("A")
@pytest.mark.parametrize(
'obj,serialized',
[(1, '\x01')],
)
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
Because we've still hard coded the association between con
and our test.
We could use parametrized fixtures:
@pytest.fixture(params=[construct.UBInt8("A")])
def con(request):
return request.param
@pytest.mark.parametrize(
'obj,serialized',
[(1, '\x01')],
)
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
But this will break if we add another construct to con
, because it'll run our parametrized inputs against all the parameters:
import pytest
import construct
@pytest.fixture(params=[construct.UBInt8("A"),
construct.UBInt16("B")])
def con(request):
return request.param
@pytest.mark.parametrize(
'obj,serialized',
[(1, '\x01')],
)
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
# def test(self, obj, serialized, con):
# > assert con.build(obj) == serialized
# E assert '\x00\x01' == '\x01'
This test already relies on the fact that constructs are immutable, so we could do this:
import pytest
import construct
UBINT8 = construct.UBInt8("A")
UBINT16 = construct.UBInt16("B")
@pytest.mark.parametrize(
'obj,serialized,con',
[
(1, '\x01', UBINT8),
(1, '\x00\x01', UBINT16),
],
)
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
But I didn't really like relying static, global "fixtures".
We could also do this:
import pytest
import construct
UBINT8 = construct.UBInt8("A")
UBINT16 = construct.UBInt16("B")
@pytest.fixture(params=[
(1, '\x01', UBINT8),
(1, '\x00\x01', UBINT16),
])
def conArgs(request):
return request.param
class TestSerialization(object):
def test(self, conArgs):
obj, serialized, con = conArgs
assert con.build(obj) == serialized
But unpacking the arguments in the test seemed hard to read.
Historically py.test hasn't had a good way to nest fixtures, which is what we really want. For that reason, and also to make selecting individual tests easier, I've favored the same approach you followed.
It turns out a py.test plugin recently became available that might help! It's called pytest-lazy-fixture. Note that it needs a very recent version of py.test -- I used 3.0.5. Here's how it allows us to rewrite our test:
import pytest
import construct
@pytest.fixture
def UBInt8A():
return construct.UBInt8("A")
@pytest.fixture
def UBInt16B():
return construct.UBInt16("B")
@pytest.mark.parametrize(
"obj,serialized,con",
[
(1, '\x01', pytest.lazy_fixture("UBInt8A")),
(1, '\x00\x01', pytest.lazy_fixture("UBInt16B")),
])
class TestSerialization(object):
def test(self, obj, serialized, con):
assert con.build(obj) == serialized
This is pretty nice, but maybe too magical. I'm also not sure how you'd just run the UBInt16 case. What do you think?
… length prefix that's not UBInt16 format.
I need this for https://github.com/pyca/tls/issues/109,
Certificate
usesUBInt32
to represent the length of thecertificate_list
.