Continuous Integration and Automated Testing
Understand why CI exists, how automated tests are structured, and how to read a pipeline log efficiently.
Integration hell and CI's solution
In 2000, a software team might spend three months developing features independently, then convene for a week-long integration sprint — a period of intense, stressful debugging as all the branches were merged and the inevitable conflicts resolved. This was called integration hell.
The observation that made CI possible was simple: if integration is painful because you do it rarely, do it constantly. If you merge every change immediately and check it automatically, integration never accumulates into a crisis.
Continuous Integration means every developer merges their changes into the shared codebase at least once per day, and every merge automatically triggers a build and test run. Problems are caught within minutes, while the change is still fresh in the developer's mind.
What CI is not
CI is frequently confused with CD (Continuous Delivery or Continuous Deployment). They are related but distinct:
| Practice | Definition | Gate |
|---|---|---|
| Continuous Integration | Frequently merging and automatically verifying changes | Tests must pass |
| Continuous Delivery | Every passing change is releasable at any time | Manual approval to release |
| Continuous Deployment | Every passing change is automatically released to production | Fully automated |
In this module we focus on CI. Delivery and deployment are covered in Module 9.
The CI feedback loop
Every CI system, regardless of which tool you use (GitHub Actions, Jenkins, CircleCI, GitLab CI), follows the same loop:
If any step returns a non-zero exit code, the pipeline fails and reports the failure. No code that fails CI can be merged into the protected main branch — this is the quality gate.
The cost of slow feedback
The value of CI is in the speed of the feedback. Consider:
| Feedback speed | Cost of fixing the bug |
|---|---|
| Within 5 minutes (CI) | Minutes — change is fresh, easy to identify |
| End of day (code review) | Hours — context lost, others may have built on top |
| Next sprint (QA testing) | Days — may require significant rework |
| In production (user report) | Very high — user impact, potential data issues, emergency fix required |
Anatomy of a test
An automated test is a program that calls your code with specific inputs and verifies the outputs match expectations. The Arrange-Act-Assert pattern gives every test a consistent structure:
def
test_calculate_discount
():
# ARRANGE: set up inputs and expected values
price = 100.0
discount_rate = 0.20
expected = 80.0
# ACT: call the code under test
result =
calculate_discount
(price, discount_rate)
# ASSERT: verify the result
assert
result == expected
The assert statement raises an AssertionError if the condition is false. pytest catches this and marks the test as failed.
Testing edge cases
Testing only the happy path (expected input, expected output) is insufficient. Good tests also cover:
- Boundary values — what happens with the smallest or largest valid input?
- Empty input — what happens with an empty list, empty string, or zero?
- Invalid input — does the function handle wrong types or values gracefully?
- Error conditions — does the function raise the right exception when it should?
import
pytest
def
test_divide_by_zero
():
# Verify the function raises ValueError for division by zero
with
pytest.raises(ValueError):
divide
(10, 0)
def
test_empty_list_returns_none
():
result =
find_max
([])
assert
result is None
def
test_single_element_list
():
result =
find_max
([42])
assert
result == 42
The testing pyramid
Not all tests are equal. The testing pyramid describes three layers of automated tests, with different trade-offs of speed, scope, and cost.
/\
/ \
/ E2E\ ← few, slow, expensive
/------\
/Integr. \ ← some, medium
/----------\
/ Unit \ ← many, fast, cheap
/______________\
| Layer | Tests | Speed | Example |
|---|---|---|---|
| Unit | Individual functions or classes in isolation | Milliseconds | test that calculate_discount(100, 0.2) returns 80.0 |
| Integration | Multiple components working together | Seconds | test that a POST /login request creates a session |
| End-to-end | The entire system from user perspective | Minutes | test that a user can sign up, log in, and place an order |
For this module, we focus on unit tests. They run in milliseconds, require no infrastructure, and give precise feedback on exactly which function is broken.
pytest in practice
pytest is Python's most popular testing library. It discovers tests automatically: any file matching test_*.py or *_test.py, and any function or method starting with test_.
# Run all tests
pytest
# Run tests in a specific file
pytest tests/test_auth.py
# Run a specific test by name
pytest tests/test_auth.py::test_login_success
# Verbose output — show each test name
pytest -v
# Stop after first failure
pytest -x
# Show local variables on failure
pytest -l
# Run tests matching a keyword
pytest -k "login or logout"
========================= test session starts ==========================
collected 5 items
tests/test_auth.py::test_login_success PASSED [ 20%]
tests/test_auth.py::test_login_wrong_password PASSED [ 40%]
tests/test_auth.py::test_login_empty_username PASSED [ 60%]
tests/test_auth.py::test_logout PASSED [ 80%]
tests/test_auth.py::test_session_expires PASSED [100%]
========================== 5 passed in 0.23s ==========================
FAILED tests/test_auth.py::test_login_wrong_password
========================= FAILURES =================================
_____________________ test_login_wrong_password ____________________
def test_login_wrong_password():
result = login('alice', 'wrong')
> assert result == False
E AssertionError: assert True == False
E (login returned True but should have returned False)
tests/test_auth.py:24: AssertionError
Fixtures and test setup
Many tests need the same setup — a database connection, a configured application, a sample user. Copying this setup into every test function is repetitive. pytest fixtures solve this:
import
pytest
# Define a fixture
@pytest.fixture
def
sample_user
():
return
{ 'username'
: 'alice'
, 'email'
: 'alice@example.com'
}
# Use the fixture as a parameter
def
test_create_greeting
(sample_user):
result =
create_greeting
(sample_user)
assert 'alice'
in result
def
test_user_has_email
(sample_user):
assert '@'
in sample_user['email'
]
Reading CI output like a pro
A CI log can be hundreds of lines. Finding the failure quickly is a critical skill. Follow this process:
- Find the step that failed — pipeline tools highlight failed steps in red. Click it to expand.
- Scroll to the first error — not the last. The first error often causes the subsequent errors.
- Identify the type of failure — is it a syntax error? An import error? A test assertion failure? An environment error?
- Note the file and line number — always shown for Python errors.
- Reproduce locally — run the exact same command the pipeline ran.
Common failure patterns
requirements.txt and push.pip install pytest to the pipeline install step.chmod +x script.sh and commit.Quality gates
A quality gate is a rule the pipeline enforces: code that fails the check cannot be merged. Common quality gates include:
- All tests must pass
- Code style must conform (e.g. flake8, black --check)
- No known security vulnerabilities in dependencies
- Test coverage does not drop below a threshold (e.g. 80%)
- No secrets or credentials in the code
Quality gates are enforced by configuring GitHub branch protection rules: a branch can require all CI checks to pass before a PR can be merged.
Key terms
Test preparation
The in-class test this week covers everything from Modules 1 through 4. It is one hour, closed-book, and counts for 10% of the module mark.
Revision checklist
- Module 1: CALMS framework, DORA four key metrics, basic navigation (
pwd,ls,cd), file permissions,chmod, environment variables - Module 2:
grepflags (-i,-r,-n,-v), pipe operator, redirection (>vs>>), shell script structure,set -e - Module 3: Git three areas (working/staging/repo),
git init,git add,git commit,git log,git diff,.gitignore - Module 4: Creating branches, merging, fast-forward vs three-way merge,
git push,git pull, pull requests, conflict markers
Sample question types
Write the Git command that creates a branch called feature/search and immediately switches to it.
git switch -c feature/search