1 The Problem
We want a Pomodoro timer: 25 minutes of work, then a 5-minute break, repeating — counting completed rounds and giving a longer break every fourth one. It teaches building a small state machine: the program moves between “work” and “break” states on a schedule.
2 How to Think About It
Think about the cycle of states, before any code:
3 The Build — explained part by part
Here is the complete timer. To keep it testable and quick, the timing is one function and the schedule logic is another. Each part is explained below.
import time
WORK = 25
SHORT_BREAK = 5
LONG_BREAK = 15
def break_length(round_number):
# Every 4th round earns a long break.
if round_number % 4 == 0:
return LONG_BREAK
return SHORT_BREAK
def wait_minutes(minutes):
# Real waiting. Tests do not call this.
time.sleep(minutes * 60)
def run(rounds=4):
for round_number in range(1, rounds + 1):
print(f"Round {round_number}: Work for {WORK} minutes.")
wait_minutes(WORK)
rest = break_length(round_number)
print(f"Break for {rest} minutes.")
wait_minutes(rest)
print(f"Done! Completed {rounds} rounds.")
if __name__ == "__main__":
run()
break_length(round_number) — the schedule rule:
round_number % 4 == 0 is true on rounds 4, 8, 12… (the % gives the remainder, which is 0 every 4th time), earning a long break. This is the state logic, isolated so we can test it.wait_minutes(minutes) — the actual waiting, kept separate so tests never have to wait.
run(rounds=4) — the main loop: for each round, work, then take the scheduled break. It moves between the work and break states each round.
round_number % 4 == 1 — that fires on rounds 1, 5, 9, not the 4th.% 4 == 0 so it fires on rounds 4, 8, 12.break_length pure and wait_minutes separate.4 Test & Prove Each Part
We test the schedule rule — which break each round gets — without any real waiting.
SHORT_BREAK = 5
LONG_BREAK = 15
def break_length(round_number):
if round_number % 4 == 0:
return LONG_BREAK
return SHORT_BREAK
def test_fourth_round_long():
"""Round 4 gets the long break."""
assert break_length(4) == LONG_BREAK
def test_early_rounds_short():
"""Rounds 1, 2, 3 get short breaks."""
assert break_length(1) == SHORT_BREAK
assert break_length(2) == SHORT_BREAK
assert break_length(3) == SHORT_BREAK
def test_eighth_round_long():
"""Round 8 also gets a long break."""
assert break_length(8) == LONG_BREAK
Run with pytest -v. By separating the schedule rule from the waiting, we test the logic instantly — no test sits through a 25-minute timer. Separating decisions from slow actions keeps code testable.
5 The Interface
What it expects
run(rounds=4)What it returns
Round 1: Work for 25 minutes.
Break for 5 minutes.
...
Round 4: Work for 25 minutes.
Break for 15 minutes.
Done! Completed 4 rounds.6 Run It & Automate It
python3 pomodoro.pyRuns real 25/5-minute cycles. Tip: shrink the constants while testing so you do not wait 25 minutes!
Jenkins runs the tests automatically — each line explained below.
Round 1: Work for 25 minutes.
Break for 5 minutes.
Round 2: Work for 25 minutes.
Break for 5 minutes.
...
Round 4: Work for 25 minutes.
Break for 15 minutes.
Done! Completed 4 rounds.% 4 == 0, not == 1.break_length, never wait_minutes or run.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Custom durations. Let the user set work and break lengths. (Teaches: input.)
- Track focus time. Total the minutes worked across a day. (Teaches: accumulating.)
- Desktop notification. Alert when a session ends. (Teaches: OS integration.)