MAD Study Hub

Home / MAD1 / Week 10

Week 10 · Testing web applications with pytest

Official syllabus line: Testing of Web Applications
About 90 minEnd Term material (see exam radar)BSCS2003 (+ project BSCS2003P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD1 weeks 4 to 9 (Flask routes, forms, Flask-SQLAlchemy models, redirects, JSON APIs). Basic Python functions and classes.

The one-minute story

Think of it like this

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, parametrize and conftest.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 ... and pytest -m ... runs;
  • explain mocking, test doubles, statement vs branch coverage, and continuous testing.

Words you need

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.slow that 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.

KindWhat it checksSpeedExample
Unitone function or method, alonevery fastsquare(5) == 25
Integrationseveral parts togetherfastroute + database model
System / end-to-endwhole app in a full environment, including the serverslowopen the real site and log in
System test automationa script that drives a real browser like a user (Selenium)slowscript clicks the Login button
Regressionre-run old tests after each changedepends"adding search did not break login"
User acceptance (beta)real users verify before releaseslowestbeta 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:

  1. Red: write a test for a feature that does not exist yet. Run it. It fails.
  2. Green: write the smallest code that makes the test pass.
  3. 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.

The idea

Arrange - Act - Assert is the shape of almost every test.

  1. Arrange: set up the data (a client, a database with one user).
  2. Act: do the one thing under test (call the function, send the request).
  3. 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.

Watch out - real exam trap
  • "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.

The idea

Test discovery rules (pytest finds tests by name; nothing else is run):

  • files named test_*.py or *_test.py;
  • inside them, functions named test_*;
  • classes named Test* (with no __init__), whose methods named test_* 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).

Solved example: 1. Predict the pytest summary
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
The idea

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):

CommandSelects
pytest -k emailtests whose name (function, class or file, any part, case-insensitive) contains email
pytest -m slowtests 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_xone specific test
pytest -vone line per test, with PASSED / FAILED
pytest -xstop after the first failure
Solved example: 2. -m and -k by prediction
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.

Solved example: 3. -k with classes and a function that is not collected
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.

Watch out - real exam trap
  • -k is a substring match on names; -m matches markers. Mixing them up is the most common error. (A detail: -k also matches marker names, so -k marker in the example above selects all three tests.)
  • Tests that do not start with test are not "deselected" and not "failed": they simply do not exist for pytest. Do not count them.
  • Count deselected only for tests that were collected and then filtered out.
  • A fixture that lives in conftest.py is shared by several files. If one test file uses a fixture that is not defined anywhere, that test shows error, not failed.

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) and client.post(url, data={...}) return a response. data={...} sends a form (like an HTML form post).
  • resp.status_code is the HTTP status (200, 302, 400, 404, 405...). resp.data is the body as bytes (b"..."); resp.get_data(as_text=True) gives text. resp.headers holds headers.
  • A redirect is not followed by default: you see the 302 and the Location header. With follow_redirects=True the 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.

Watch out - real exam trap
  • Test client vs the requests library. 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. The test_client() needs no server. A paper question runs app.py and the test in two terminals: only then does requests pass.
  • resp.data is bytes: resp.data == "Hello" is False; use b"Hello".
  • Without follow_redirects=True the status after a redirect is 302, not 200.
  • Forgetting the in-memory database means tests write into the real .db file.

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:

DoubleWhat it does
Dummyfills a parameter, never really used
Stubreturns fixed answers
Fakea simple working version (the in-memory SQLite database is a fake database)
Mockrecords how it was called, so you can assert "it was called once"
Spywraps 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).

Solved example: 4. Which tests go where

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.

The idea

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 if goes both true and false. foo(1,1) and foo(1,0) give it.
  • Condition coverage: every individual condition (x > 0, y > 0) takes both values. foo(0,1) and foo(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()
In your project

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

Your turn: 1. Test discovery

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.

Your turn: 2. Predict -k
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.

Your turn: 3. conftest and fixtures

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.

Your turn: 4. Write a client test

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 == 404
Your turn: 5. Late binding (a tricky paper question)
powers = []
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

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 -v lines like test3 FAILED. About a dozen real questions look like this.
  • -k and -m selection (about 7 questions): which command gives 1 passed, 2 deselected? Output of pytest -k Test_class. Fixture plus -k email gives 1 passed, 1 deselected. Marker questions with and and not.
  • 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.
Watch out - real exam trap
  • Names matter: case_test2 and sample_test3 are not collected. Do not count them in the summary.
  • -k for names, -m for markers; deselected appears 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

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__.py
  • conftest.py
  • requirements.txt
pytest loads 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
The test client does not follow redirects unless told to; it shows the 302.

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
That is the standard matching used in the papers.

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().
The test client calls the app in memory, so no server is needed, and 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
An if without else can run its body in every test and still never take the false path.

Viva: say it out loud

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

Remember this
  • 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_*.py files, test_* functions, Test* classes. Names that do not start with test are ignored.
  • Fixtures prepare data; conftest.py shares them; yield gives teardown; parametrize repeats a test with many inputs.
  • -k selects by name substring, -m by marker; filtered-out tests are deselected.
  • 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.
Code and facts to keep
  • 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 (use yield for 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
Connected weeks (learn them together)

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