Chapitre 26
Writing functions and testing a developed program
Lecture : 19 minQuiz de 12 questions
# Writing functions and testing a developed program
## Objectifs et prérequis
This chapter aims to equip Lower Sixth ICT students with the ability to design, write, test, and debug functions and procedures in a structured programming context. Students will learn to decompose problems into reusable code blocks, apply parameter passing mechanisms, validate program correctness through systematic testing, and produce reliable software solutions relevant to local and African contexts. Prerequisites include familiarity with basic programming constructs (variables, data types, control structures: sequence, selection, iteration) and experience writing simple programs in a high-level language such as Python or Pascal.
## Introduction
In modern software development, writing functions and procedures is fundamental to creating maintainable, scalable, and efficient programs. This chapter builds on prior knowledge of basic programming to introduce modular programming concepts. Students will learn how to encapsulate logic into functions and procedures, pass data between them using parameters and return values, and test their programs rigorously to ensure correctness. The focus is on practical application, with examples drawn from Cameroonian and African realities—such as calculating agricultural yields, managing student records in schools, or processing mobile money transactions—to reinforce relevance. By the end of this chapter, students will be able to design modular programs, write and test functions and procedures effectively, and apply testing strategies to identify and fix errors.
## I. Functions and procedures 1
### Context
Modular programming emerged as a response to the complexity of large-scale software systems. In the 1960s and 1970s, pioneers like Edsger Dijkstra and Niklaus Wirth advocated for breaking programs into smaller, manageable units. This approach improves readability, reduces redundancy, and facilitates team collaboration. In Cameroonian secondary education, introducing functions and procedures at the Lower Sixth level prepares students for tertiary ICT studies and real-world problem-solving in sectors like agriculture, health, and finance, where customized software solutions are increasingly needed.
### Definitions
> A **function** is a named block of code that performs a specific task and returns a value to the caller.
> A **procedure** (or subroutine) is a named block of code that performs a specific task but does not return a value.
> **Parameter passing** is the mechanism by which data is transferred into a function or procedure when it is called.
> A **local variable** is a variable declared inside a function or procedure, accessible only within that block.
> A **global variable** is a variable declared outside all functions, accessible throughout the program.
> **Scope** refers to the region of a program where a variable can be accessed.
### Facts and Concepts
Functions and procedures promote code reuse. For example, a function to calculate the area of a rectangle can be used in multiple parts of a program without rewriting the logic. Parameters allow functions to be flexible: the same function can process different inputs. In Cameroon, a procedure might be used to print a student’s report card, taking the student’s name and scores as parameters, without returning a value. Functions are preferred when a result is needed for further computation (e.g., calculating tax on income), while procedures are used for actions like displaying output or updating a file.
Parameter passing mechanisms include:
- **Pass by value**: A copy of the argument is passed; changes inside the function do not affect the original variable.
- **Pass by reference**: The function accesses the original variable; changes affect the caller’s variable.
In Python (commonly used in Cameroonian schools), arguments are passed by object reference. For immutable types (like integers, strings), changes inside the function do not affect the original (simulating pass by value). For mutable types (like lists), changes inside the function affect the original (simulating pass by reference).
### Examples
**Cameroonian context**: A function to calculate the Body Mass Index (BMI) for health screening in rural clinics:
```python
def calculate_bmi(weight_kg, height_m):
return weight_kg / (height_m ** 2)
```
This function takes weight and height as parameters and returns the BMI. It can be reused across multiple patient records.
**African context**: A procedure to display welcome messages in multiple African languages for a community information kiosk:
```python
def display_welcome(language):
if language == "French":
print("Bienvenue")
elif language == "English":
print("Welcome")
elif language == "Swahili":
print("Karibu")
else:
print("Hello")
```
This procedure does not return a value; it performs the action of printing a greeting based on the input language.
### Vocabulary to retain
- Function
- Procedure
- Parameter
- Argument
- Return value
- Local variable
- Global variable
- Scope
- Pass by value
- Pass by reference
### Example exam question and plan
**Question**: Write a Python function that calculates the total cost of purchasing `n` bags of fertilizer, where each bag costs 2500 FCFA, and applies a 10% discount if more than 10 bags are bought. The function should return the final cost. Write a procedure that displays the result in the format: "Total cost: X FCFA".
**Plan**:
1. Define the function `calculate_fertilizer_cost(n)`:
- Parameter: `n` (number of bags)
- If `n > 10`, apply 10% discount: `total = n * 2500 * 0.9`
- Else: `total = n * 2500`
- Return `total`
2. Define the procedure `display_cost(cost)`:
- Parameter: `cost` (the returned value from the function)
- Print: `"Total cost: " + str(cost) + " FCFA"`
3. In the main program:
- Ask user for number of bags
- Call function to get cost
- Call procedure to display result
## II. Functions and procedures 2
### Context
Building on basic function and procedure concepts, this lesson explores advanced aspects such as default parameters, multiple return values, recursion, and modular program design. As ICT systems in Cameroon grow more complex—e.g., in e-governance platforms like the Cameroon Online Tax System or mobile health (mHealth) initiatives—students must understand how to design flexible and robust modules. This lesson also addresses common pitfalls, such as unintended side effects from global variables and infinite recursion.
### Definitions
> A **default parameter** is a parameter in a function definition that is assigned a default value if no argument is provided during the call.
> **Multiple return values** occur when a function returns more than one value, typically as a tuple or list.
> **Recursion** is a technique where a function calls itself to solve a smaller instance of the same problem.
> A **base case** in recursion is the condition that stops the recursive calls.
> **Side effect** refers to any change in state outside the local environment of a function, such as modifying a global variable.
> **Modular design** is the practice of dividing a program into independent, interchangeable modules (functions/procedures) that each perform a distinct task.
### Facts and Concepts
Default parameters increase function flexibility. For example, a function to calculate interest might have a default rate of 5% unless overridden. In Cameroon, a procedure for generating invoices could have a default tax rate of 19.25% (the standard VAT rate), adjustable for special cases.
Multiple return values allow a function to convey related information. For instance, a function that processes exam scores might return both the average and the highest score. In Python, this is done by returning a tuple: `return average, highest`.
Recursion is powerful for problems with self-similar structure, such as calculating factorials or traversing file directories. However, it must include a base case to prevent infinite recursion and stack overflow. An example is computing the factorial of a number: `n! = n * (n-1)!`, with base case `0! = 1`.
Side effects, especially from modifying global variables, can make programs unpredictable and hard to debug. Best practice favors using parameters and return values to pass data, minimizing reliance on globals.
Modular design enhances maintainability. A program for managing a school’s student database might have separate modules: one for inputting data, one for calculating grades, one for generating reports, and one for saving to file. Each module can be tested and updated independently.
### Examples
**Cameroonian context**: A function to calculate compound interest with a default annual rate:
```python
def compound_interest(principal, years, rate=0.05):
amount = principal * (1 + rate) ** years
return amount
```
Called as `compound_interest(100000, 3)` uses the default 5% rate; `compound_interest(100000, 3, 0.07)` overrides it.
**African context**: A recursive function to calculate the total number of nodes in a binary tree representing a family lineage (used in genealogical studies in West Africa):
```python
def count_nodes(node):
if node is None:
return 0 # Base case
return 1 + count_nodes(node.left) + count_nodes(node.right)
```
This function returns the size of the tree by recursively counting left and right subtrees.
A procedure with a side effect to avoid (bad practice):
```python
total_score = 0
def add_score(points):
global total_score
total_score += points # Side effect: modifies global variable
```
Better approach: return the updated score and let the caller manage state:
```python
def add_score(current_total, points):
return current_total + points
```
### Vocabulary to retain
- Default parameter
- Multiple return values
- Recursion
- Base case
- Side effect
- Modular design
- Immutable
- Mutable
- Tuple
- Stack overflow
### Example exam question and plan
**Question**: Write a recursive Python function that calculates the nth Fibonacci number, where F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2) for n > 1. Then, write a procedure that displays the first `n` Fibonacci numbers in a comma-separated string.
**Plan**:
1. Define the recursive function `fibonacci(n)`:
- Base case: if `n == 0`, return 0; if `n == 1`, return 1
- Recursive case: return `fibonacci(n-1) + fibonacci(n-2)`
2. Define the procedure `display_fibonacci_series(n)`:
- Initialize an empty list `series`
- Loop from `i = 0` to `n-1`:
- Append `str(fibonacci(i))` to `series`
- Join the list with commas: `result = ", ".join(series)`
- Print `result`
3. In the main program:
- Ask user for `n`
- Call the procedure to display the series
## III. Testing a program 1
### Context
Testing is a critical phase in the software development lifecycle. As programs grow in complexity, ensuring they behave correctly under various conditions becomes essential. In Cameroon, where ICT solutions are deployed in resource-constrained environments (e.g., offline health clinics, rural agricultural extension services), undetected bugs can have serious consequences. This lesson introduces the fundamentals of program testing: types of errors, testing strategies, and the importance of test cases. Students learn to distinguish between syntax, runtime, and logic errors, and to design tests that verify correctness.
### Definitions
> A **syntax error** is a violation of the programming language’s grammar rules, detected during compilation or interpretation.
> A **runtime error** (or exception) occurs during program execution due to invalid operations, such as division by zero or accessing an invalid memory location.
> A **logic error** is a mistake in the program’s algorithm that produces incorrect output despite the program running without crashing.
> A **test case** is a set of inputs, execution conditions, and expected results designed to verify a particular program behavior.
> **Black-box testing** evaluates functionality without knowledge of internal code structure, based solely on specifications.
> **White-box testing** examines internal logic, paths, and structures of the code.
> **Debugging** is the process of identifying, analyzing, and removing errors from a program.
### Facts and Concepts
Syntax errors prevent the program from running (e.g., missing colon, misspelled keyword). Runtime errors occur during execution (e.g., trying to convert a non-numeric string to an integer). Logic errors are the most insidious: the program runs but gives wrong results (e.g., using `=` instead of `==` in a condition, or off-by-one errors in loops).
In Cameroon, a logic error in a program calculating crop yield predictions could lead to poor farming decisions. A runtime error in a mobile money transaction system could cause financial loss. Testing aims to catch these before deployment.
Black-box testing focuses on inputs and outputs: “Given this input, what output do we expect?” White-box testing looks at code coverage: “Have we tested every branch of this if-statement?”
A good test case includes:
- Normal (typical) inputs
- Boundary (edge) inputs (e.g., minimum, maximum, zero)
- Invalid inputs (to test error handling)
For example, testing a function that calculates tax on income:
- Normal: income = 500,000 FCFA
- Boundary: income = 0, income = 1,000,000 FCFA (if tax brackets change)
- Invalid: income = -1000 (should trigger error handling)
### Examples
**Cameroonian context**: A procedure to convert Celsius to Fahrenheit for weather reporting in Douala:
```python
def celsius_to_fahrenheit(c):
return (c * 9/5) + 32
```
Test cases:
- Normal: c = 25 → expected 77.0
- Boundary: c = 0 (freezing) → expected 32.0; c = 100 (boiling) → expected 212.0
- Invalid: c = -300 (below absolute zero) → should handle gracefully (e.g., return error or clamp)
**African context**: A function to validate a Ugandan mobile money transaction ID (format: starts with "TXN", followed by 8 digits):
```python
def is_valid_txn_id(txn_id):
if len(txn_id) != 11:
return False
if txn_id[:3] != "TXN":
return False
if not txn_id[3:].isdigit():
return False
return True
```
Test cases:
- Normal: "TXN12345678" → True
- Boundary: "TXN00000000" → True; "TXN99999999" → True
- Invalid: "TXN1234567" (too short) → False; "txn12345678" (lowercase) → False; "TXN123456789" (too long) → False
### Vocabulary to retain
- Syntax error
- Runtime error
- Logic error
- Test case
- Black-box testing
- White-box testing
- Debugging
- Boundary value
- Equivalence partitioning
- Test data
### Example exam question and plan
**Question**: A program is designed to calculate the average score of a student in three subjects. The user enters three scores (each between 0 and 20). The program should display the average and a remark: "Excellent" if average ≥ 16, "Good" if 12 ≤ average < 16, "Fair" if 8 ≤ average < 12, and "Weak" if average < 8. Identify and correct the logic error in the following code:
```python
score1 = float(input("Enter score 1: "))
score2 = float(input("Enter score 2: "))
score3 = float(input("Enter score 3: "))
average = (score1 + score2 + score3) / 3
if average >= 16:
print("Excellent")
elif average >= 12:
print("Good")
elif average >= 8:
print("Fair")
else:
print("Weak")
```
**Plan**:
1. Identify the error: The conditions are correct, but the remark for "Fair" should be for average < 12 and ≥ 8, which is correct. Wait—actually, the code is correct? Let’s recheck:
- ≥16: Excellent
- ≥12 and <16: Good (since if ≥16, first condition catches it)
- ≥8 and <12: Fair (since if ≥12, second catches it)
- <8: Weak
This code is actually correct. To introduce a logic error, suppose the code had:
```python
if average > 16: # Should be >=
print("Excellent")
elif average > 12:
print("Good")
elif average > 8:
print("Fair")
else:
print("Weak")
```
Then, a score of exactly 16 would fall through to "Good" instead of "Excellent".
2. Corrected version:
```python
score1 = float(input("Enter score 1: "))
score2 = float(input("Enter score 2: "))
score3 = float(input("Enter score 3: "))
average = (score1 + score2 + score3) / 3
if average >= 16:
print("Excellent")
elif average >= 12:
print("Good")
elif average >= 8:
print("Fair")
else:
print("Weak")
```
3. Test cases to verify:
- Normal: 16, 16, 16 → average 16 → "Excellent"
- Boundary: 12, 12, 12 → average 12 → "Good"
- Boundary: 8, 8, 8 → average 8 → "Fair"
- Boundary: 7.9, 8, 8 → average ~7.97 → "Weak"
- Invalid: -1, 10, 10 → should be handled (but code doesn’t validate input; extension: add input validation)
## IV. Testing a program 2
### Context
Effective testing requires systematic strategies to maximize error detection with minimal effort. This lesson builds on testing fundamentals to introduce structured techniques: equivalence partitioning, boundary value analysis, and test case design based on specifications. In Cameroon’s growing tech ecosystem—where startups develop solutions for fintech, agritech, and edtech—rigorous testing ensures reliability and user trust. Students learn to create test plans, document test results, and use testing to improve program quality, not just to find bugs.
### Definitions
> **Equivalence partitioning** divides input data into classes where the system is expected to behave similarly; one test case from each class is sufficient.
> **Boundary value analysis** focuses on testing values at the edges of equivalence classes, as errors often occur there.
> A **test plan** is a document outlining the scope, approach, resources, and schedule of testing activities.
> **Test data** refers to the specific inputs used to execute test cases.
> **Regression testing** retests previously tested functionality after modifications to ensure no new bugs were introduced.
> **User acceptance testing (UAT)** involves end-users validating that the system meets their requirements.
> A **defect** (or bug) is any flaw in the software that causes it to fail to meet its requirements.
### Facts and Concepts
Equivalence partitioning reduces redundant testing. For a function accepting scores from 0 to 20:
- Valid partition: 0–20 (test one value, e.g., 10)
- Invalid partitions: <0 (e.g., -1) and >20 (e.g., 21)
Boundary value analysis targets the edges: 0, 20, -1, 21. These are more likely to reveal errors than middle values.
A test plan ensures testing is organized and traceable. For a school management system, it might include:
- Scope: testing grade calculation and report generation
- Approach: black-box testing using specifications
- Resources: 2 testers, 1 week
- Schedule: test design (Mon), execution (Tue–Thu), reporting (Fri)
Regression testing is vital when updating software. For example, after fixing a bug in a tax calculation module, regression testing ensures the fix didn’t break invoice generation.
UAT is crucial in Cameroonian contexts where end-users (e.g., teachers, farmers) may not be technical. Involving them ensures the solution is usable and relevant—e.g., a mobile app for cocoa farmers must work on low-end phones and in local languages.
Defects are documented with: ID, description, steps to reproduce, expected vs. actual result, severity, and status.
### Examples
**Cameroonian context**: A function to determine if a student is eligible for a scholarship based on average score (≥14) and attendance (≥80%):
```python
def is_eligible(average, attendance):
return average >= 14 and attendance >= 80
```
Equivalence partitions:
- Valid: average ≥14 and attendance ≥80 → test (15, 85)
- Invalid average: average <14 → test (13, 85)
- Invalid attendance: attendance <80 → test (15, 75)
- Both invalid: (13, 75)
Boundary values:
- Average: 14 (min valid), 13.9 (max invalid), 20 (max valid), 20.1 (invalid)
- Attendance: 80 (min valid), 79.9 (max invalid), 100 (max valid), 100.1 (invalid)
Test cases:
- Normal: (15, 85) → True
- Boundary: (14, 80) → True; (13.9, 80) → False; (15, 79.9) → False
- Invalid: (-1, 50) → False; (25, 90) → False
**African context**: A procedure to calculate the price of a basket of goods in a Kenyan market, applying a 5% discount if total > 1000 KES:
```python
def calculate_market_price(items):
total = sum(items)
if total > 1000:
total *= 0.95
return total
```
Test plan:
- Scope: test discount application and summation
- Approach: black-box testing
- Test data:
- Normal: [200, 300, 400] → sum 900 → no discount → expected 900
- Boundary: [400, 400, 200.01] → sum 1000.01 → discount → expected 950.0095
- Boundary: [333.33, 333.33, 333.33] → sum 999.99 → no discount → expected 999.99
- Invalid: [-100, 500] → should handle negative (extension: validate inputs)
### Vocabulary to retain
- Equivalence partitioning
- Boundary value analysis
- Test plan
- Test data
- Regression testing
- User acceptance testing (UAT)
- Defect
- Severity
- Priority
- Test script
### Example exam question and plan
**Question**: Design a test plan for a function that converts a student’s score (0–100) to a grade: A (90–100), B (80–89), C (70–79), D (60–69), F (<60). Include equivalence partitioning, boundary value analysis, and at least four test cases with expected results.
**Plan**:
1. **Equivalence partitioning**:
- Valid partitions:
- A: 90–100 → test value 95
- B: 80–89 → test value 85
- C: 70–79 → test value 75
- D: 60–69 → test value 65
- F: 0–59 → test value 50
- Invalid partitions:
- <0 → test value -5
- >100 → test value 105
2. **Boundary value analysis**:
- For A: 90 (min), 89.9 (max invalid for B), 100 (max), 100.1 (invalid)
- For B: 80, 79.9, 89.9, 90
- For C: 70, 69.9, 79.9, 80
- For D: 60, 59.9, 69.9, 70
- For F: 0, -0.1, 59.9, 60
3. **Test cases** (selecting key boundaries):
- Test 1: score = 90 → expected grade A
- Test 2: score = 89.9 → expected grade B
- Test 3: score = 60 → expected grade D
- Test 4: score = 59.9 → expected grade F
- Test 5: score = -5 → expected invalid (or error)
- Test 6: score = 105 → expected invalid
4. **Test plan summary**:
- Scope: Validate grade conversion logic
- Approach: Black-box testing using specifications
- Test data: As above
- Expected outcome: All valid inputs return correct grade; invalid inputs are handled appropriately
## Synthèse du chapitre
- Functions and procedures enable modular, reusable, and maintainable code by encapsulating logic into named blocks.
- Functions return values; procedures perform actions without returning values.
- Parameter passing allows data exchange: pass by value (copy) and pass by reference (access to original).
- Scope determines variable accessibility: local (inside block) vs. global (throughout program).
- Advanced features include default parameters, multiple return values, recursion (with base case), and avoiding side effects.
- Testing identifies syntax, runtime, and logic errors; black-box and white-box techniques complement each other.
- Effective test design uses equivalence partitioning and boundary value analysis to maximize coverage.
- A test plan organizes testing activities; regression testing ensures stability after changes.
- User acceptance testing ensures solutions meet real-world needs, especially in Cameroonian and African contexts.
- Debugging is the iterative process of locating and fixing defects to improve program quality.
## Glossaire
- **Function**: A named block of code that performs a task and returns a value.
- **Procedure**: A named block of code that performs a task but does not return a value.
- **Parameter**: A variable in a function/procedure definition that receives data from the caller.
- **Argument**: The actual value passed to a parameter during a function/procedure call.
- **Return value**: The output sent back by a function to its caller.
- **Local variable**: A variable declared inside a function/procedure, accessible only within that block.
- **Global variable**: A variable declared outside all functions, accessible throughout the program.
- **Scope**: The region of a program where a variable can be accessed.
- **Recursion**: A technique where a function calls itself to solve a smaller instance of the same problem.
- **Base case**: The condition in recursion that stops further recursive calls.
- **Equivalence partitioning**: A testing technique dividing input data into classes of expected similar behavior.
- **Boundary value analysis**: A testing technique focusing on values at the edges of input domains.
- **Test case**: A set of inputs, conditions, and expected results used to verify program behavior.
- **Test plan**: A document outlining the scope, approach, resources, and schedule of testing activities.
- **Defect**: A flaw in the software causing it to fail to meet requirements.
- **Debugging**: The process of identifying, analyzing, and removing errors from a program.
Quiz du chapitre
12 questions — vérifie chaque réponse pour voir l'explication.