Week 10 · Testing web applications with pytest
The one-minute story
Testing is the quality check at a biscuit factory. Before a packet leaves the factory, somebody checks it. One person checks that a single biscuit is not broken (that is a unit test). Another checks that biscuits, wrapper and box fit together (an integration test). A third opens a sealed box exactly as a customer would and tastes it (an end-to-end test). If you checked by hand every time you changed the recipe, you would get tired and skip some checks. So you write the checks as small programs that run in seconds, and you run them after every change.
pytest is the machine that runs all your check-programs and prints "passed" or "failed". The Flask test client is a pretend browser that lives inside your Python program, so you can click links and submit forms without opening a real browser or starting a real server.
By the end of this week you can:
- explain why we test and name the kinds of tests (unit, integration, system, regression, acceptance);
- write pytest tests with
assert, fixtures,parametrizeandconftest.py; - test Flask pages and JSON APIs with
app.test_client()against a separate in-memory SQLite database; - predict the exact summary line of
pytest -k ...andpytest -m ...runs; - explain mocking, test doubles, statement vs branch coverage, and continuous testing.
Words you need
- Test - a small program that runs part of your code and checks the result.
- Unit test - tests one small unit (a function or method) alone.
- Integration test - tests several units working together (a route plus the database).
- System test (end-to-end) - tests the whole running application, server included, from the outside.
- Regression test - a test that is re-run after every change to make sure old features did not break.
- User acceptance test (UAT) - real users try the software before release (alpha and beta testing).
- White box testing - tests designed by looking at the code inside. Black box: only inputs and outputs are looked at.
- TDD - test-driven development: write the failing test first, then the code.
- Fixture - a function that prepares something a test needs (an app, a client, test data).
- Mock - a fake object that replaces a real one (a web service, a clock) so the test is fast and predictable.
- Test double - the general name for any stand-in: dummy, stub, fake, mock, spy.
- Coverage - the percentage of your code that the tests actually executed.
- Marker - a label such as
@pytest.mark.slowthat lets you pick which tests to run.
Idea 1: Why test, and the kinds of tests
Without tests you find bugs when a user complains. With tests you find them in seconds, on your own laptop. Tests also let you change code without fear: if all tests still pass, you probably broke nothing.
| Kind | What it checks | Speed | Example |
|---|---|---|---|
| Unit | one function or method, alone | very fast | square(5) == 25 |
| Integration | several parts together | fast | route + database model |
| System / end-to-end | whole app in a full environment, including the server | slow | open the real site and log in |
| System test automation | a script that drives a real browser like a user (Selenium) | slow | script clicks the Login button |
| Regression | re-run old tests after each change | depends | "adding search did not break login" |
| User acceptance (beta) | real users verify before release | slowest | beta testers use the app |
Testing pyramid. Write many unit tests (cheap, fast), fewer integration tests, and very few end-to-end tests (slow, fragile). The picture is a pyramid with unit tests as the wide base.
White box vs black box. If you design tests by reading the code (every if branch must run), it is white box testing. If you only know "this input should give that output", it is black box testing.
Test-driven development (TDD) is a habit with three steps, repeated in short loops:
- Red: write a test for a feature that does not exist yet. Run it. It fails.
- Green: write the smallest code that makes the test pass.
- Refactor: clean the code while keeping all tests green.
Continuous testing. Tests are run automatically on every push (see CI in week 12). Continuous integration means changes from many people are merged into one code base often and tested each time. Continuous delivery means that after the tests pass, a release package is prepared automatically. Continuous deployment goes one step further: every passing change is put into production automatically.
Arrange - Act - Assert is the shape of almost every test.
- Arrange: set up the data (a client, a database with one user).
- Act: do the one thing under test (call the function, send the request).
- Assert: check the result (
assert status_code == 200).
Keep one idea per test. A test name should say what is checked: test_add_requires_title.
- "System testing automation" (scripting a real browser) is not the same as a unit test. In the match-the-column questions: unit = smallest logical units; integration = several modules together; system = whole app with server; automation = simulated user browser interaction; acceptance = real users before release; white box = internal functioning; regression = runs after every change.
- Beta testing = user acceptance testing. Integration testing is one step before system testing.
Idea 2: pytest basics
Install: pip install pytest. pytest is the tool; the tests are plain functions with plain assert.
# file: test_cases.py
def square(n):
return n * n
def test1():
assert square(5) == 25
def test2():
assert square(square(2)) == 16
def test3():
assert square(square(2)) == 8
Run pytest -v test_cases.py. Real output (trimmed):
test_cases.py::test1 PASSED [ 33%]
test_cases.py::test2 PASSED [ 66%]
test_cases.py::test3 FAILED [100%]
FAILED test_cases.py::test3 - assert 16 == 8
========================= 1 failed, 2 passed in 0.09s =========================
square(square(2)) is square(4) which is 16, so test3 fails and pytest shows both sides of the broken comparison: assert 16 == 8.
Test discovery rules (pytest finds tests by name; nothing else is run):
- files named
test_*.pyor*_test.py; - inside them, functions named
test_*; - classes named
Test*(with no__init__), whose methods namedtest_*are tests.
A function called case_test2 or sample_test3 is silently ignored: it is not collected, not run, not counted. A test passes if it finishes without an exception; a failed assert raises AssertionError and the test is marked failed.
What the summary line means. Common words: passed, failed, deselected (collected but skipped because of -k or -m), error (a problem outside the test body, such as a missing fixture), warnings.
# test_class_demo.py
class TestGroup:
def do_something(self, x, y): # helper, not a test (no test_ prefix)
return x % y
def test_one(self):
assert self.do_something(10, 5) == 0, "Is 5 factor of 10?"
def test_two(self):
assert self.do_something(32, 5) == 0, "Is 5 factor of 32?"
pytest -v prints TestGroup::test_one PASSED and TestGroup::test_two FAILED; the failure line carries your message after AssertionError:. Only the failed test's message is shown; the passing test's message never appears. Summary: 1 failed, 1 passed.
Useful pytest pieces
import pytest
def test_float():
assert 0.1 + 0.2 == pytest.approx(0.3) # never compare floats with ==
def test_error():
with pytest.raises(ZeroDivisionError): # passes only if the error is raised
1 / 0
def test_tmp(tmp_path): # built-in fixture: a fresh temp folder
f = tmp_path / "notes.txt"
f.write_text("hi")
assert f.read_text() == "hi"
tmp_path gives every test its own empty temporary folder that is cleaned up later, so tests that write files never disturb each other or your real files. pytest also has monkeypatch (temporarily replace a variable or function) and capsys (capture print output).
def dec(x):
return x - 1
def test_answer():
assert dec(3) == 3
Question. What does pytest report?
Solution. dec(3) is 2, so assert 2 == 3 fails. Output: FAILED test1.py::test_answer - assert 2 == 3 and 1 failed. It is "failed", not "passed", and the count is 1, not 2.
Idea 3: Fixtures, conftest.py, parametrize, markers, -k and -m
A fixture prepares something and hands it to the test. A test asks for it by writing the fixture's name as a parameter.
import pytest
@pytest.fixture
def students():
return {"names": ["ravi", "raj"], "ages": [12, 9]}
def test_name(students):
assert "ravi" in students["names"]
def test_age(students):
assert students["ages"][0] > 9
Each test gets a fresh copy (the default scope is per test), so one test cannot damage another's data. Fixtures that need cleanup use yield: the code before yield is the setup, the value after yield is given to the test, and the code after yield runs as teardown.
@pytest.fixture
def numbers():
print("setup")
yield [1, 2, 3]
print("teardown") # runs after the test finishes
conftest.py is a special file. Fixtures written in it are available to every test file in that folder and below, with no import. Put shared fixtures (app, client) there. A fixture defined inside one test file is visible only to that file.
Parametrize runs one test with many inputs:
@pytest.mark.parametrize("a,b,expected", [(2, 3, 5), (0, 0, 0), (-1, 1, 0)])
def test_add_numbers(a, b, expected):
assert a + b == expected
This is three tests, shown as test_add_numbers[2-3-5], test_add_numbers[0-0-0], test_add_numbers[-1-1-0]. Real run: all pass.
Markers label tests: @pytest.mark.slow. Select with -m. Register your markers in pytest.ini to avoid warnings:
[pytest]
markers =
slow: tests that take long
unit: unit tests
Selecting tests (this is what the papers ask the most):
| Command | Selects |
|---|---|
pytest -k email | tests whose name (function, class or file, any part, case-insensitive) contains email |
pytest -m slow | tests carrying the marker slow |
pytest -m "not slow" | everything without the marker |
pytest -m "slow and integration" | tests carrying both markers |
pytest test_a.py::test_x | one specific test |
pytest -v | one line per test, with PASSED / FAILED |
pytest -x | stop after the first failure |
import pytest
def square(x):
sum = 0
for counter in range(x):
sum += x
return sum # this really computes x times x
@pytest.mark.marker1
def testcase_1(): assert square(10) == 100
@pytest.mark.marker2
def testcase_2(): assert square(4) == 4
@pytest.mark.marker3
def testcase_3(): assert square(5) == 25
Question. What is the summary of pytest -m marker2? Of pytest -m marker1?
Solution. square(x) adds x a total of x times, so square(4) is 16, and assert 16 == 4 fails. With -m marker2 only that one test runs: 1 failed, 2 deselected. With -m marker1: square(10) is 100, so 1 passed, 2 deselected (real run confirmed). The unselected tests are never run, so their outcome does not matter, but they are counted as deselected.
def func(x, y):
return x**2 + y**2
class Test_class0:
def test_case1(self): assert func(1, 2) == 5 # 1+4 = 5, pass
def case_test2(self): assert func(2, 3) == 13 # not collected
def test_case3(self): assert func(6, 2) == 38 # 36+4 = 40, fail
class Test_class1:
def test_case1(self): assert func(5, 2) == 29 # 25+4 = 29, pass
def case_test2(self): assert func(1, 1) == 2 # not collected
Question. Output of pytest test_file.py -k Test_class? Of -k Test_class0?
Solution. Only three methods are collected (test_case1, test_case3, and Class1.test_case1); the two case_test2 methods do not start with test. -k Test_class matches both class names, so nothing is deselected: 1 failed, 2 passed. With -k Test_class0 only the first class is picked: 1 failed, 1 passed, 1 deselected. Real run confirmed both.
-kis a substring match on names;-mmatches markers. Mixing them up is the most common error. (A detail:-kalso matches marker names, so-k markerin the example above selects all three tests.)- Tests that do not start with
testare not "deselected" and not "failed": they simply do not exist for pytest. Do not count them. - Count
deselectedonly for tests that were collected and then filtered out. - A fixture that lives in
conftest.pyis shared by several files. If one test file uses a fixture that is not defined anywhere, that test showserror, notfailed.
Idea 4: Testing Flask with the test client
app.test_client() returns an object that behaves like a browser but calls your app directly in memory. No server, no port, very fast.
Here is the small app we test (an application factory, so a test can build its own app):
# app.py
from flask import Flask, jsonify, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80), nullable=False)
def create_app(config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo.db"
if config:
app.config.update(config) # a test can override the database
db.init_app(app)
with app.app_context():
db.create_all()
@app.route("/")
def home():
return "Todo app"
@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title", "").strip()
if not title:
return "Title required", 400
db.session.add(Todo(title=title))
db.session.commit()
return redirect(url_for("home"))
@app.route("/api/todos", methods=["GET", "POST"])
def todos():
if request.method == "POST":
data = request.get_json()
if not data or "title" not in data:
return jsonify(error="title missing"), 400
t = Todo(title=data["title"])
db.session.add(t)
db.session.commit()
return jsonify(id=t.id, title=t.title), 201
return jsonify([{"id": t.id, "title": t.title} for t in Todo.query.all()])
return app
The key trick: a separate in-memory database for tests. If tests used todo.db, they would fill your real data with junk. The URI sqlite:///:memory: creates a database that lives only in RAM and vanishes when the app object goes away. Every test gets a fresh, empty database. (Flask-SQLAlchemy 3 keeps one shared connection for in-memory SQLite, so the tables you create stay visible.)
# conftest.py
import pytest
from app import create_app, db
@pytest.fixture
def app():
app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"})
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
TESTING=True makes Flask let exceptions propagate to the test instead of hiding them behind a 500 page. A fixture can use another fixture (client asks for app).
# test_app.py
def test_home(client):
resp = client.get("/")
assert resp.status_code == 200
assert b"Todo" in resp.data # resp.data is BYTES
def test_add_requires_title(client):
resp = client.post("/add", data={"title": " "})
assert resp.status_code == 400
def test_add_redirects(client):
resp = client.post("/add", data={"title": "milk"})
assert resp.status_code == 302
assert resp.headers["Location"] == "/"
def test_add_follow(client):
resp = client.post("/add", data={"title": "milk"}, follow_redirects=True)
assert resp.status_code == 200
assert resp.data == b"Todo app"
All of these passed in a real run. Notes:
client.get(url)andclient.post(url, data={...})return a response.data={...}sends a form (like an HTML form post).resp.status_codeis the HTTP status (200, 302, 400, 404, 405...).resp.datais the body as bytes (b"...");resp.get_data(as_text=True)gives text.resp.headersholds headers.- A redirect is not followed by default: you see the 302 and the
Locationheader. Withfollow_redirects=Truethe client follows it and you see the final page. - Posting to a route that allows only GET gives 405; an unknown URL gives 404.
Testing JSON API endpoints
def test_api_create_and_list(client):
r = client.post("/api/todos", json={"title": "milk"}) # json= sends a JSON body
assert r.status_code == 201
assert r.get_json() == {"id": 1, "title": "milk"}
r = client.get("/api/todos")
assert r.get_json() == [{"id": 1, "title": "milk"}]
def test_api_bad(client):
r = client.post("/api/todos", json={})
assert r.status_code == 400
assert r.get_json()["error"] == "title missing"
def test_isolated(client):
assert client.get("/api/todos").get_json() == [] # fresh database again
json= sets the Content-Type: application/json header and encodes the body; r.get_json() decodes the reply. The id is 1 in the first test, and test_isolated sees an empty list, proving each test has its own database. Whole file: 10 passed.
- Test client vs the
requestslibrary.requests.get("http://127.0.0.1:5000/...")talks to a real running server. If the server is not started in another terminal, the test fails with a connection error. Thetest_client()needs no server. A paper question runsapp.pyand the test in two terminals: only then doesrequestspass. resp.datais bytes:resp.data == "Hello"is False; useb"Hello".- Without
follow_redirects=Truethe status after a redirect is 302, not 200. - Forgetting the in-memory database means tests write into the real
.dbfile.
Idea 5: Mocking and test doubles
A good unit test must be fast, repeatable and independent of the outside world. Calling a real weather service, sending a real e-mail or reading the real clock breaks all three. So we replace those parts with fakes. All the fakes are called test doubles:
| Double | What it does |
|---|---|
| Dummy | fills a parameter, never really used |
| Stub | returns fixed answers |
| Fake | a simple working version (the in-memory SQLite database is a fake database) |
| Mock | records how it was called, so you can assert "it was called once" |
| Spy | wraps the real thing and records the calls |
# weather.py
import requests
def temperature(city):
r = requests.get("https://api.example.com/temp", params={"city": city}, timeout=5)
r.raise_for_status()
return r.json()["celsius"]
def describe(city):
t = temperature(city)
return "hot" if t >= 30 else "ok"
# test_weather.py
from unittest.mock import patch, MagicMock
import weather
@patch("weather.requests.get") # replace requests.get inside weather.py
def test_describe_hot(mock_get):
mock_get.return_value = MagicMock(status_code=200)
mock_get.return_value.json.return_value = {"celsius": 35}
assert weather.describe("Chennai") == "hot"
mock_get.assert_called_once()
def test_describe_ok(monkeypatch):
monkeypatch.setattr(weather, "temperature", lambda city: 20)
assert weather.describe("Ooty") == "ok"
Both pass in a real run, with no internet used. The golden rule: patch where the name is used (weather.requests.get), not where it was defined. Mocks are also the way to force rare cases (a timeout, a 500 from a service).
Question. Classify: (a) assert is_prime(7); (b) POST a form to /add and check the row in the in-memory database; (c) a Selenium script logs in on the running site and clicks "Logout"; (d) friends try your beta; (e) re-run everything after adding search.
Solution. (a) unit; (b) integration (route + database); (c) system test automation (UI test); (d) user acceptance (beta); (e) regression.
Idea 6: Code coverage, UI tests, and the habit of running tests
Code coverage answers "how much of my code did the tests execute?". With the pytest-cov plugin:
pip install pytest-cov
pytest --cov=app --cov-branch --cov-report=term-missing
Real output on our app with the 10 tests above:
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------
app.py 38 0 8 1 98% 14->16
----------------------------------------------------
TOTAL 38 0 8 1 98%
All 38 statements ran (Miss 0), but one of 8 branches was only taken one way: line 14 is if config:, and our tests always pass a config, so the "no config" path was never taken (14->16 means "from line 14 jump to line 16" was never seen). That is why statement coverage is 100 percent yet total coverage shows 98 percent once branches count.
Coverage kinds, using if x > 0 and y > 0: z = x:
- Function coverage: every function is called at least once.
- Statement (line) coverage: every statement runs.
foo(1,1)is enough. - Branch coverage: every
ifgoes both true and false.foo(1,1)andfoo(1,0)give it. - Condition coverage: every individual condition (
x > 0,y > 0) takes both values.foo(0,1)andfoo(1,0)give each condition a False and a True.
100 percent statement coverage does not imply 100 percent branch coverage (an if with no else can run its body but never test the false side), and it does not imply condition coverage. High coverage does not mean no bugs; it only means the lines were run, not that the asserts were good.
UI testing with Selenium (concept). Selenium lets a script start a real browser, open your site, fill the form, click buttons and read the page. It tests what a user sees (HTML, CSS, JavaScript together). It is slow and fragile (a changed button label breaks it), so you write few of them. In the syllabus it is "system testing automation".
# concept only; needs a browser driver installed
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("http://127.0.0.1:5000/login")
driver.find_element(By.NAME, "email").send_keys("a@b.com")
driver.find_element(By.ID, "login-btn").click()
assert "Dashboard" in driver.title
driver.quit()
For your project viva. Add a tests/ folder with a conftest.py (app + client fixtures on an in-memory database) and at least five tests: one page loads (200), a bad form gives 400, login works, an API returns JSON, and a database row is created. Run pytest --cov and be ready to show it live. The examiner will ask: "How do you know your app works after a change?" and "Why do your tests not touch your real database?"
Your turn
A file test_x.py has def test_a(), def check_b(), def testc(), and class Helper with a method test_d. Which tests does pytest run?
Need a hint?
Look at the names and the class name.
Show the full solution
test_a (name starts with test_) and testc (pytest's function prefix is test, so testc also matches). check_b does not start with test; Helper does not start with Test, so its test_d is not collected. So 2 tests run.
def test_user_add(): assert True
def test_user_del(): assert False
def test_item_add(): assert True
Output summary for pytest -k user? For pytest -k add?
Need a hint?
Count names containing the word.
Show the full solution
-k user: two tests selected (test_user_add passes, test_user_del fails), one deselected: 1 failed, 1 passed, 1 deselected. -k add: test_user_add and test_item_add, both pass, one deselected: 2 passed, 1 deselected.
conftest.py has a fixture divisible_by_39 returning 39. File A has test_div3 (x % 3 == 0) and test_div6 (x % 6 == 0) using it. File B has test_div13 (x % 13 == 0) using it. Summary of pytest?
Need a hint?
No import is needed for conftest fixtures. Evaluate each remainder.
Show the full solution
39 % 3 = 0 (pass), 39 % 6 = 3 (fail), 39 % 13 = 0 (pass). So 1 failed, 2 passed; the fixture is shared across both files with no import and no errors.
Write a test that checks GET /nothing returns 404 for the todo app, using the client fixture.
Need a hint?
Use client.get and the status code.
Show the full solution
def test_missing_page(client):
assert client.get("/nothing").status_code == 404powers = []
for i in range(1, 5):
def powers_of_x(x):
return x ** i
powers.append(powers_of_x)
powers_of_3 = [f(3) for f in powers]
Tests assert 9 in powers_of_3, 27 in powers_of_3, 64 in powers_of_3. Summary?
Need a hint?
When does a function look up i?
Show the full solution
A function looks up i when it is called, and by then the loop has ended with i = 4. So every function computes 3**4 = 81, and powers_of_3 is [81, 81, 81, 81]. None of 9, 27, 64 is in it: 3 failed (real run confirmed).
Exam radar
The keyword match for this week finds about 45 distinct graded questions, almost all in End Term (about 177 keyword hits in End Term papers against about 3 in Quiz 2 and none in Quiz 1, so this is purely an End Term topic and the count is coarse because the keyword also catches unrelated questions). The real recurring questions are:
- Predict the pytest summary for a small test file:
1 failed, 2 passed,3 failed, with-vlines liketest3 FAILED. About a dozen real questions look like this. -kand-mselection (about 7 questions): which command gives1 passed, 2 deselected? Output ofpytest -k Test_class. Fixture plus-k emailgives1 passed, 1 deselected. Marker questions withandandnot.- Fixtures and conftest.py shared by several files (one question asks for the summary
1 failed, 2 passed). - Match the type of testing with its description (unit, integration, system, system testing automation, user acceptance, regression, white box). About 5 real questions, the same table repeated.
- Coverage match: function / statement / branch / condition coverage for a small C function, and the true/false statement that 100 percent statement coverage does not imply branch coverage.
- Continuous integration and continuous delivery statements (both correct).
- Test client vs requests: a live-server test passes only if the app runs in another terminal.
- Names matter:
case_test2andsample_test3are not collected. Do not count them in the summary. -kfor names,-mfor markers;deselectedappears only when a filter is used.- With
-m "not slow", count carefully which tests lack the marker. - An assert message (the text after the comma) is shown only for the tests that fail. A passing test prints nothing.
- Closures in loops (late binding) make every function use the last
i. - Statement coverage 100 percent is not branch coverage 100 percent.
Quick quiz
A file contains def test_a(): assert 1 == 1, def check_b(): assert 1 == 2 and def test_c(): assert 2 == 2. What is the summary of running pytest?
- 1 failed, 2 passed
- 3 passed
- 2 passed
- 2 passed, 1 deselected
check_b does not start with test, so it is not collected, not failed and not deselected. Only two tests run.A fixture that should be available to every test file in a folder is best placed in...
- every test file, copied
__init__.pyconftest.pyrequirements.txt
conftest.py automatically and shares its fixtures with all tests in that folder and below.The test client.post("/add", data={"title": "milk"}) returns a redirect. Without follow_redirects=True, resp.status_code is...
- 200
- 302
- 404
- 201
Match each test type with its focus: unit, integration, system test automation, user acceptance.
- unit: server and environment; integration: one function; automation: real users; acceptance: browser script
- unit: smallest function; integration: several modules together; automation: simulated browser interaction; acceptance: real users before release
- unit: real users; integration: browser script; automation: single function; acceptance: several modules
- all four are the same thing
Select all TRUE statements about Flask tests with pytest. (select all that apply)
- Test functions must start with
test_to be collected. - Fixtures help give each test an isolated setup.
- Routes cannot be tested with
client.get(). - You must start the Flask server before using
app.test_client().
client.get() is exactly how routes are tested.Which is TRUE?
- 100 percent statement coverage always means 100 percent branch coverage
- 100 percent statement coverage does not imply 100 percent branch coverage
- Code coverage proves the program has no bugs
- Branch coverage is weaker than statement coverage
if without else can run its body in every test and still never take the false path.Viva: say it out loud
Why did you write tests for your project, and what kinds?
Tests let me change the code without fear of breaking old features. I wrote unit tests for helper functions and integration tests using Flask's test client for the routes and the database. I run them with pytest after every change.
How do your tests avoid damaging your real data?
The app fixture builds the app with a test configuration that uses an in-memory SQLite database, sqlite:///:memory:. Every test starts with a fresh empty database that disappears afterwards, so my real file is never touched.
What is a fixture, and what is conftest.py?
A fixture is a function that prepares what a test needs, such as the app or a logged-in client, and the test asks for it by name. conftest.py is where I keep fixtures that all test files can use without importing them.
What is the difference between the Flask test client and Selenium?
The test client calls the Flask app directly in memory without a browser, so it is fast and tests the server side. Selenium drives a real browser and tests what the user sees, including JavaScript, but it is slower and more fragile.
What is mocking and when did you use it?
Mocking replaces a real dependency, like an external API or the clock, with a fake object so the test is fast and predictable. I patch the name where it is used and then assert on what the code did with the fake.
Explain statement coverage and branch coverage.
Statement coverage is the share of lines that ran at least once. Branch coverage asks that each decision was taken both ways, true and false. So you can have full statement coverage and still miss an else path.
What is TDD?
Test-driven development: first write a failing test for a small feature, then write just enough code to pass it, then clean up. It keeps the code testable and the tests meaningful.
Recap
- Test types: unit (one function), integration (parts together), system / end-to-end (whole app), system test automation (script a browser, Selenium), regression (after every change), acceptance / beta (real users). Pyramid: many unit, few end-to-end.
- pytest collects
test_*.pyfiles,test_*functions,Test*classes. Names that do not start withtestare ignored. - Fixtures prepare data;
conftest.pyshares them;yieldgives teardown;parametrizerepeats a test with many inputs. -kselects by name substring,-mby marker; filtered-out tests aredeselected.- Flask:
app.test_client(),client.get/post,status_code,data(bytes),get_json(),follow_redirects=True; use a separate in-memory SQLite database. - Mocks and other test doubles replace slow or unpredictable parts. Patch where the name is used.
- Coverage: function, statement, branch, condition; 100 percent statement is not 100 percent branch.
- Arrange - Act - Assert; TDD is red, green, refactor; CI runs tests on every push.
pip install pytest pytest-cov; run:pytest,pytest -v,pytest -x,pytest file.py::test_name- Selection:
-k name_part,-m marker,-m "not slow",-m "a and b" - Fixture:
@pytest.fixture(useyieldfor teardown); parametrize:@pytest.mark.parametrize("a,b", [(1,2),(3,4)]) - Checks:
assert x == y,pytest.approx(0.3),with pytest.raises(Err):,tmp_path - Flask:
app.test_client();client.get("/");client.post("/x", data={...});client.post("/api", json={...}) - Response:
.status_code,.data(bytes),.get_json(),.headers["Location"] - Test database:
{"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"} - Mock:
from unittest.mock import patch, MagicMock;@patch("module.name") - Coverage:
pytest --cov=app --cov-branch --cov-report=term-missing - Summary words: passed, failed, deselected, error, warnings
- MAD2 week 12 - The project: putting it all together: Test the API with pytest before the Vue app depends on it.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All End Term papers