TDD in Python With Pytest
Click here to load reader
-
Upload
eddy-reyes -
Category
Software
-
view
634 -
download
3
Transcript of TDD in Python With Pytest
Overview
● High-level discussion of TDD● TDD walk-through with pytest● Mocking with pytest
Not looking to proselytize TDD in this presentation● I’m just presenting the concepts and method.
Further ReadingUncle Bob Martin● http://blog.8thlight.com/uncle-bob/archive.htmlKent Beck● Extreme Programming Explained (book)Is TDD Dead? video series● https://www.youtube.com/watch?v=z9quxZsLcfo (part 1)Code From This Presentation● https://github.com/ereyes01/apug-pytest-prez
What Is TDD?
● Methodology of Implementing Software● It is NOT a silver bullet!● It is NOT the only way to write good
software!○ But, if followed, will help you write solid software!
Effective TDD● TDD Method- To change a Program:
○ Write a unit test, watch it fail○ Change the code to make the test pass○ Rinse, repeat...
● Unit tests are effective when they are self-contained○ No external dependencies
● TDD is not for prototyping!○ Use when you fully understand your design and how
to code your solution
Anatomy of a Test
● Given… precondition● When… X happens● Then… Y must be true
Tests == Formal Design Spec● Make your tests as readable as you would a (formal)
specification document.
Python TDD Tools● Standard library
○ unittest○ unittest.mock
■ As of Python 3.3● Nosetests● pytest
○ http://www.pytest.org
Testing With Pytest● No classes needed!● Fully takes advantage of Python’s dynamism to help
you design beautiful tests.● Use plain Python assert instead of Xunit jargon● Fixtures
○ Makes it easy to define your test preconditions○ Fixtures can be nested arbitrarily, allowing complex
dependency trees○ Cleanup is handled gracefully
Pytest Fixture Examplefrom my_flask_app import create_app
@pytest.fixture
def app():
app = create_app("test")
return app
@pytest.fixture
def app_client(app):
client = app.test_client()
return client
# GIVEN: app_clientdef test_hello_route(app_client):
# WHEN:reply = app_client.get(“/hello”)
# THEN:assert reply.data == “Hello World”
Easy Test Dependencies
● Fixtures allow arbitrarily nested test dependencies, eliminate DRY in your tests!
○ Compare with unittest... fixtures look like:class TestSomething(unittest.TestCase):
def setUp():# fixture code here
def tearDown():# cleanup fixture here
def testSomething():# test case code here
Example: TDD and Flask Hello World
● Let’s walk through how we would implement the Flask Hello World example using TDD.○ http://flask.pocoo.org
● Requirements:○ Need a Flask app○ Must reply the text “hello world” to a GET of the
“/hello” route.
Need to Experiment?
● Not yet sure how to build this?○ Stop your TDD!○ Play, read docs learn, experiment…○ Build a prototype if you like
…● Do NOT commit that code!
○ TDD is not for learning… it’s for executing on something you already know how to build.
Step 1: Start With A Testtest_hello.py:def test_hello():
"""
GIVEN: A flask hello app
WHEN: I GET the hello/ route
THEN: The response should be "hello world"
"""
assert True
Step 2: Define Test Dependencies
test_hello.pyimport pytest
import hello_app
@pytest.fixture
def app():
return hello_app.app
@pytest.fixture
def test_client(app):
return app.test_client()
def test_hello(test_client):"""GIVEN: A flask hello appWHEN: I GET the hello/ routeTHEN: The response should be "hello world""""assert True
Step 2 Cont’d
Step 3: Add hello_app Module
hello_app.pyimport flask
app = flask.Flask(__name__)
Step 4: Add Test For /hello Route
test_hello.pyimport pytest
import hello_app
@pytest.fixture
def app():
return hello_app.app
@pytest.fixture
def test_client(app):
return app.test_client()
def test_hello(test_client):"""GIVEN: A flask hello appWHEN: I GET the hello/ routeTHEN: The response should be "hello world""""response = test_client.get("/hello")assert response.data.decode("utf-8") == "hello world"
Step 4 Cont’d
Step 5: Add The /hello Route
hello_app.pyimport flask
app = flask.Flask(__name__)
@app.route("/hello")
def hello():
return "hello world"
We’re Done!
Congratulations, you’ve just followed TDD to create a Flask hello world web application!
Real Life is Never That Simple!
● Of course it’s not● Applications connect to the network,● Use databases,● Do I/O on enormous files,● etc.
Mocking The Edges Of Your App
● Mocks are a testing technique to stub out the “edges” of your application○ “Edges” == external components
● You don’t want to test external components out of your control○ Network○ Database○ Large Files
Mocking with Pytest’s monkeypatch● Pytest defines a special fixture called monkeypatch● Allows arbitrary setattr on anything in your tested
code’s namespace● Example:
def test_unknown_file(monkeypatch): monkeypatch.setattr("os.path.islink", lambda x: False) monkeypatch.setattr("os.path.isdir", lambda x: False)
mylib.check_file("/some/path" )
● Monkeypatched symbols are restored in test cleanup
Mocks as Your Own Fixtures● monkeypatch can be nested within your own fixtures to
define high-level dependencies
● Helps you write clean test code with mocks that follows the pattern of Given-When-Then
● Mocks help your application code remain separate from your testing mechanisms.
Let’s Extend Our Flask Example
● We will add a new route:○ /hacker_news_encoding○ This route returns the “Content-Encoding” header
value returned by the Hacker News site● We can’t directly test Hacker News
○ Site could change○ Site could be down○ Unreliable test results
Step 6: Add a Test For The RouteMOCK_ENCODING = “mock-encoding”
def test_encoding_header(test_client, mock_encoding_request ):
"""
GIVEN: A flask hello app
A mock request handler
WHEN: I GET the /hacker_news_encoding route
THEN: The response should be the expected Content-Encoding
"""
response = test_client.get("/hacker_news_encoding")
assert response.data.decode("utf-8") == MOCK_ENCODING
Step 7: Add The Mock Fixtureclass MockEncodingResponse:
def __init__(self):
self.headers = {"Content-Encoding": MOCK_ENCODING}
def _mock_get(url):
assert url == "https://news.ycombinator.com"
return MockEncodingResponse()
@pytest.fixture
def mock_encoding_request(monkeypatch):
monkeypatch.setattr("requests.get", _mock_get)
Step 7 Cont’d
Step 8: Add The New Routehello_app.pyimport flask
import requests
app = flask.Flask(__name__)
@app.route("/hello")
def hello():
return "hello world"
@app.route("/hacker_news_encoding")
def hacker_news_encoding():
url = "https://news.ycombinator.com"
resp = requests.get(url)
return response.headers["Content-Encoding"]
Step 8 Cont’d
Want The Code?
Fork me on Github!
https://github.com/ereyes01/apug-pytest-prez