Migrating from pytest¶
This guide is for developers who are familiar with pytest and want to understand how Specter concepts map to what they already know.
Core Differences¶
Concept |
pytest |
Specter |
|---|---|---|
Test suite |
Module / class |
|
Test case |
|
Any public method |
Nested suites |
Not supported natively |
Nested |
Setup (per suite) |
|
|
Setup (per test) |
|
|
Assertion |
|
|
Fatal assertion |
|
|
Non-fatal assert |
|
|
Skip |
|
|
Conditional skip |
|
|
Parameterize |
|
|
Shared fixtures |
|
|
Tagging / marking |
|
|
Example Comparison¶
pytest:
import pytest
class TestCalculator:
def setup_method(self):
self.calc = Calculator()
def test_addition(self):
assert self.calc.add(1, 2) == 3
def test_raises(self):
with pytest.raises(ZeroDivisionError):
self.calc.divide(1, 0)
@pytest.mark.parametrize('a,b,expected', [
(1, 2, 3),
(10, 20, 30),
])
def test_add_parametrized(a, b, expected):
assert Calculator().add(a, b) == expected
Specter equivalent:
from specter import Spec, DataSpec, expect
class CalculatorSpec(Spec):
def before_each(self):
self.calc = Calculator()
def it_adds_two_numbers(self):
expect(self.calc.add(1, 2)).to.equal(3)
def it_raises_on_divide_by_zero(self):
expect(lambda: self.calc.divide(1, 0)).to.raise_a(ZeroDivisionError)
class AdditionSpec(DataSpec):
DATASET = {
'small': {'a': 1, 'b': 2, 'expected': 3},
'large': {'a': 10, 'b': 20, 'expected': 30},
}
def it_adds_correctly(self, a, b, expected):
expect(Calculator().add(a, b)).to.equal(expected)
Key Behavioral Differences¶
Non-fatal assertions¶
The most significant behavioral difference is how assertion failures are
handled. In pytest, any assert failure immediately stops the test. In
Specter, expect() records the failure but lets the test continue. This
allows you to see all failures in a single test run.
Use require() when you want pytest-style immediate failure, for example
when a later assertion would error (not just fail) if an earlier value is
wrong:
require(response).not_to.be_none()
expect(response.status_code).to.equal(200) # only runs if response is not None
Test naming¶
pytest requires test functions to be prefixed with test_. Specter has no
such restriction: any public method (no leading underscore) on a Spec is
a test. This means names like it_creates_a_user or
when_the_database_is_empty are perfectly valid.
Test discovery¶
pytest discovers files matching test_*.py or *_test.py. Specter
discovers all Python files inside the spec/ directory (or whichever path
you pass to --search).
Fixtures vs. fixture decorator¶
pytest fixtures are functions decorated with @pytest.fixture and injected
via parameter names. Specter fixtures are base classes decorated with
@fixture. They are not executed directly as tests but provide shared
tests and lifecycle hooks to every class that inherits them.
from specter import Spec, fixture, expect
@fixture
class DatabaseFixture(Spec):
def before_each(self):
self.db = connect_to_test_db()
def after_each(self):
self.db.close()
class UserRepoSpec(DatabaseFixture):
def it_creates_a_user(self):
user = self.db.users.create(name='Alice')
expect(user.id).not_to.be_none()
Running Selected Tests¶
Task |
Command |
|---|---|
Run all tests |
|
Run a specific module |
|
Run tests by name |
|
Run tests by tag |
|
Run with keyword (pytest -k equiv) |
|