Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #46 from darrenburns/expect-expansion
Browse files Browse the repository at this point in the history
Expect expansion
  • Loading branch information
darrenburns authored Oct 22, 2019
2 parents daa003f + 58a357a commit 4d09ade
Show file tree
Hide file tree
Showing 6 changed files with 459 additions and 90 deletions.
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Planned features:
* Handling flaky tests with test-specific retries, timeouts
* Integration with unittest.mock (specifics to be ironed out)
* Plugin system

re
## Getting Started

Install Ward with `pip install ward`.
Expand Down Expand Up @@ -92,16 +92,10 @@ def test_database_connection(database):
expect(users).contains("Bob")
```

### The Expect API
### The `expect` API

In the (contrived) `test_capital_cities` test, we want to determine whether
the `get_capitals_from_server` function is behaving as expected,
so we grab the output of the function and pass it to `expect`. From
here, we check that the response is as we expect it to be by chaining
methods. If any of the checks fail, the expect chain short-circuits,
and the remaining checks won't be executed for that test. Methods in
the Expect API are named such that they correspond as closely to standard
Python operators as possible, meaning there's not much to memorise.
Use `expect` to perform tests on objects by chaining together methods. Using `expect` allows Ward
to provide detailed, highly readable output when your tests fail.

```python
from ward import expect, fixture
Expand All @@ -119,6 +113,24 @@ def test_capital_cities(cities):
.equals(cities))
```

Most methods on `expect` have inverted equivalents, e.g. `not_equals`, `not_satisfies`, etc.

### Working with mocks

`expect` works well with `unittest.mock`, by providing methods such as `expect.called`, `expect.called_once_with`,
and more. If a test fails due to the mock not being used as expected, Ward will print specialised output to aid
debugging the problem.

```python
from ward import expect
from unittest.mock import Mock

def test_mock_was_called():
mock = Mock()
mock(1, 2, x=3)
expect(mock).called_once_with(1, 2, x=3)
```

### Checking for exceptions

The test below will pass, because a `ZeroDivisionError` is raised. If a `ZeroDivisionError` wasn't raised,
Expand Down Expand Up @@ -187,8 +199,8 @@ considered a failure.
Check that a value is close to another value.

```python
expect(1.0).approx(1.01, epsilon=0.2) # pass
expect(1.0).approx(1.01, epsilon=0.001) # fail
expect(1.0).approx(1.01, abs_tol=0.2) # pass
expect(1.0).approx(1.01, abs_tol=0.001) # fail
```

### Cancelling a run after a specific number of failures
Expand Down
249 changes: 234 additions & 15 deletions tests/test_expect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from ward import expect
from ward.expect import Expected, ExpectationFailed
from unittest.mock import Mock, patch, call

from ward import expect, fixture, raises
from ward.expect import Expected, ExpectationFailed, math


def test_equals_success_history_recorded():
Expand All @@ -15,15 +17,18 @@ def test_equals_failure_history_recorded():
this, that = "hello", "goodbye"

e = expect(this)
try:
with raises(ExpectationFailed):
e.equals(that)
except ExpectationFailed:
pass

hist = [Expected(this=this, op="equals", that=that, op_args=(), op_kwargs={}, success=False)]
expect(e.history).equals(hist)


def test_equals_failure_ExpectationFailed_raised():
with raises(ExpectationFailed):
expect(1).equals(2)


def test_satisfies_success_history_recorded():
this = "olleh"
predicate = lambda e: this[::-1] == "hello"
Expand All @@ -39,35 +44,249 @@ def test_satisfies_failure_history_recorded():
predicate = lambda e: False

e = expect(this)
try:
with raises(ExpectationFailed):
e.satisfies(predicate)
except ExpectationFailed:
pass

hist = [Expected(this=this, op="satisfies", that=predicate, op_args=(), op_kwargs={}, success=False)]

expect(e.history).equals(hist)


def test_satisfies_failure_ExpectationFailed_raised():
with raises(ExpectationFailed):
expect(1).satisfies(lambda x: False)


def test_identical_to_succeeds_when_things_are_identical():
expect(ZeroDivisionError).identical_to(ZeroDivisionError)


def test_identical_to_fails_when_things_are_not_identical():
with raises(ExpectationFailed):
expect(ZeroDivisionError).identical_to(AttributeError)


def test_approx_success_history_recorded():
this, that, eps = 1.0, 1.01, 0.5

e = expect(this).approx(that, epsilon=eps)
e = expect(this).approx(that, abs_tol=eps)

hist = [Expected(this=this, op="approx", that=that, op_args=(), op_kwargs={"epsilon": eps}, success=True)]
hist = [
Expected(
this=this, op="approx", that=that, op_args=(), op_kwargs={"rel_tol": 1e-09, "abs_tol": 0.5}, success=True
)
]
expect(e.history).equals(hist)


def test_approx_failure_history_recorded():
this, that, eps = 1.0, 1.01, 0.001

# This will raise an ExpectationFailed, which would fail this test unless we catch it.
e = expect(this)
try:
with raises(ExpectationFailed):
e.approx(that, eps)
except ExpectationFailed:
pass

hist = [Expected(this=this, op="approx", that=that, op_args=(eps,), op_kwargs={}, success=False)]
hist = [
Expected(
this=this, op="approx", that=that, op_args=(), op_kwargs={"rel_tol": 0.001, "abs_tol": 0.0}, success=False
)
]

expect(e.history).equals(hist)


@fixture
def isclose():
with patch.object(math, "isclose", autospec=True) as m:
yield m


def test_approx_delegates_to_math_isclose_correctly(isclose):
this, that = 1.0, 1.1
abs_tol, rel_tol = 0.1, 0.2

expect(this).approx(that, abs_tol=abs_tol, rel_tol=rel_tol)

expect(isclose).called_once_with(this, that, abs_tol=abs_tol, rel_tol=rel_tol)


def test_not_approx_delegeates_to_isclose_correctly(isclose):
this, that = 1.0, 1.2
abs_tol = 0.01

with raises(ExpectationFailed):
expect(this).not_approx(that, abs_tol=abs_tol)

expect(isclose).called_once_with(this, that, abs_tol=abs_tol, rel_tol=1e-9)


def test_not_equals_success_history_recorded():
this, that = 1, 2

e = expect(this).not_equals(that)

hist = [Expected(this, op="not_equals", that=that, op_args=(), op_kwargs={}, success=True)]
expect(e.history).equals(hist)


def test_not_equals_failure_history_recorded():
this, that = 1, 1

e = expect(this)
with raises(ExpectationFailed):
e.not_equals(that)

hist = [Expected(this, op="not_equals", that=that, op_args=(), op_kwargs={}, success=False)]
expect(e.history).equals(hist)


@fixture
def mock():
return Mock()


def test_mock_called_when_mock_was_called(mock):
mock()
e = expect(mock).called()

hist = [Expected(mock, op="called", that=None, op_args=(), op_kwargs={}, success=True)]
expect(e.history).equals(hist)


def test_mock_called_when_mock_wasnt_called(mock):
e = expect(mock)
with raises(ExpectationFailed):
e.called()

hist = [Expected(mock, op="called", that=None, op_args=(), op_kwargs={}, success=False)]
expect(e.history).equals(hist)


def test_mock_not_called_success(mock):
e = expect(mock).not_called()

hist = [Expected(mock, op="not_called", that=None, op_args=(), op_kwargs={}, success=True)]
expect(e.history).equals(hist)


def test_mock_called_once_with_success(mock):
args = (1, 2, 3)
kwargs = {"hello": "world"}
mock(*args, **kwargs)

e = expect(mock).called_once_with(*args, **kwargs)

hist = [Expected(mock, op="called_once_with", that=None, op_args=args, op_kwargs=kwargs, success=True)]
expect(e.history).equals(hist)


def test_mock_called_once_with_failure_missing_arg(mock):
args = (1, 2, 3)
kwargs = {"hello": "world"}
mock(1, 2, **kwargs) # 3 is missing intentionally

e = expect(mock)
with raises(ExpectationFailed):
e.called_once_with(*args, **kwargs)

hist = [Expected(mock, op="called_once_with", that=None, op_args=args, op_kwargs=kwargs, success=False)]
expect(e.history).equals(hist)


def test_mock_called_once_with_failure_missing_kwarg(mock):
args = (1, 2, 3)
kwargs = {"hello": "world"}
mock(*args, wrong="thing")

e = expect(mock)
with raises(ExpectationFailed):
e.called_once_with(*args, **kwargs)

hist = [Expected(mock, op="called_once_with", that=None, op_args=args, op_kwargs=kwargs, success=False)]
expect(e.history).equals(hist)


def test_mock_called_once_with_fails_when_multiple_correct_calls(mock):
args = (1, 2, 3)
kwargs = {"hello": "world"}

mock(*args, **kwargs)
mock(*args, **kwargs)

e = expect(mock)
with raises(ExpectationFailed):
e.called_once_with(*args, **kwargs)

hist = [Expected(mock, op="called_once_with", that=None, op_args=args, op_kwargs=kwargs, success=False)]
expect(e.history).equals(hist)


def test_mock_called_once_with_fails_when_multiple_calls_but_one_correct(mock):
args = (1, 2, 3)
kwargs = {"hello": "world"}

mock(1)
mock(*args, **kwargs)
mock(2)

e = expect(mock)
with raises(ExpectationFailed):
e.called_once_with(*args, **kwargs)

hist = [Expected(mock, op="called_once_with", that=None, op_args=args, op_kwargs=kwargs, success=False)]
expect(e.history).equals(hist)


def test_called_with_succeeds_when_expected_call_is_last(mock):
mock(1)
mock(2)

e = expect(mock).called_with(2)
expect(e.history[0].success).equals(True)


def test_called_with_fails_when_expected_call_is_made_but_not_last(mock):
mock(2)
mock(1)
e = expect(mock)
with raises(ExpectationFailed):
e.called_with(2)
expect(e.history[0].success).equals(False)


def test_has_calls_succeeds_when_all_calls_were_made(mock):
mock(1, 2)
mock(key="value")

e = expect(mock).has_calls([call(1, 2), call(key="value")])
expect(e.history[0].success).equals(True)


def test_has_calls_fails_when_not_all_calls_were_made(mock):
mock(1, 2)

e = expect(mock)
with raises(ExpectationFailed):
e.has_calls([call(1, 2), call(key="value")])
expect(e.history[0].success).equals(False)


def test_has_calls_fails_when_calls_were_made_in_wrong_order(mock):
mock(1, 2)
mock(key="value")

e = expect(mock)
with raises(ExpectationFailed):
e.has_calls([call(key="value"), call(1, 2)])


def test_has_calls_succeeds_when_all_calls_were_made_any_order(mock):
mock(1, 2)
mock(key="value")

e = expect(mock).has_calls(
[call(key="value"), call(1, 2)],
any_order=True,
)

expect(e.history[0].success).equals(True)
3 changes: 2 additions & 1 deletion tests/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_i_fail():
expected_result = TestResult(test=test, outcome=TestOutcome.FAIL, error=mock.ANY, message="")

expect(result).equals(expected_result)
expect(result.error).is_instance_of(AssertionError)
expect(result.error).instance_of(AssertionError)


def test_generate_test_runs__yields_skipped_test_result_on_test_with_skip_marker(
Expand Down Expand Up @@ -127,3 +127,4 @@ def my_test(fix_a, fix_b):
list(suite.generate_test_runs())

expect(events).equals([1, 2, 3])

Loading

0 comments on commit 4d09ade

Please sign in to comment.