This is a ready-to-use unit test record for GAMP Category 5 custom software: a script, module, or function set built for your own use rather than a configured commercial product. Complete one record per function or module, per version tested, before that version proceeds to OQ. Replace every <<FILL: ...>> placeholder with your own specifics and route the completed record through your normal review and retention process. A filled specimen follows. This content is educational reference, not legal or regulatory advice; verify each cited regulation against the current source before you rely on it.
For the lifecycle this record sits inside, see validating custom software: GAMP Category 5 from specification to release. This record documents unit testing at the component level; it does not replace OQ, which verifies the integrated system against the functional specification, or the independent code review, which is a separate record; see independent code review of a GxP script for that control.
The anti-circularity rule, stated plainly
An expected output derived from running the code being tested is not a unit test. It is a description of what the code does, signed as if it were proof that the code is correct. This is one of the most common findings in Category 5 inspections: a team runs the function, records whatever it outputs as the “expected” value, and signs the record as a pass. Nothing was verified, because the code was never checked against anything independent of itself.
The expected output in every row of section 4 must come from one of the following, and the source must be named:
- A hand calculation, worked through on paper or in a spreadsheet, independent of the code under test.
- An independent tool or library that implements the same calculation by a different route.
- A previously validated system’s output for the same input, where that system is itself a suitable reference.
- A published reference value, for a standard test case with a known answer.
If none of these is available for a given function, the function is not yet specified precisely enough to be tested, and the design specification (DS) needs more detail before testing proceeds.
Record control header
| Field | Entry |
|---|---|
| Record title | Unit Test Record |
| Record number | <<FILL: UT-ID, e.g. UT-YLDCALC-2026-003>> |
| System / tool name | <<FILL>> |
| Design specification reference | <<FILL: DS-ID and version>> |
| Version / commit under test | <<FILL: tag or commit hash>> |
| Test date(s) | <<FILL>> |
| Tester | <<FILL: name, role>> |
| Independent reviewer | <<FILL: name, role, confirms not the author of the code under test>> |
1. Purpose
This record documents unit-level testing of individual functions or modules in <<FILL: system / tool name>>, at the version identified above, before that version proceeds to integration and OQ. Unit testing catches defects at the point where they are cheapest to fix and creates the evidence that each function was verified against an independently derived expected result, not against its own output.
2. Scope
In scope: the functions and modules listed in section 4, at the version under test. Out of scope: integration behavior, the full application under real deployment conditions, and end-to-end workflow, all of which are covered by OQ and PQ, not by this record. <<FILL: name any function deliberately excluded from unit testing and why, e.g. a thin wrapper with no logic of its own>>.
3. Coverage requirement
For every function that performs a calculation or a decision (a branch, a threshold comparison, a classification), test at minimum:
- Normal case(s): typical valid inputs, with a known-answer expected output.
- Boundary case(s): the minimum valid input, the maximum valid input, and any threshold value the function compares against, tested at, just below, and just above.
- Invalid case(s): inputs that should be rejected, confirming the specified error is raised and no partial or silent result is produced.
A function with only normal-case coverage has not been tested against the conditions most likely to reveal a defect.
4. Unit test log
| Test ID | Function / module | Version tested | Case type | Test inputs | Expected output | Independent source of expected output | Actual output | Pass/Fail | Tester | Date |
|---|---|---|---|---|---|---|---|---|---|---|
<<FILL: UT-001>> | <<FILL>> | <<FILL>> | Normal | <<FILL>> | <<FILL>> | <<FILL: hand calc / independent tool / reference value>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> |
<<FILL: UT-002>> | <<FILL>> | <<FILL>> | Boundary, minimum | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> |
<<FILL: UT-003>> | <<FILL>> | <<FILL>> | Boundary, maximum | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> |
<<FILL: UT-004>> | <<FILL>> | <<FILL>> | Boundary, at threshold | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> |
<<FILL: UT-005>> | <<FILL>> | <<FILL>> | Invalid | <<FILL>> | <<FILL: specific error expected>> | <<FILL: DS error-handling spec>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> |
Add rows for every function in scope; do not compress multiple functions into one row. A test case marked pass with no independent source named for the expected output does not satisfy section 1’s purpose, regardless of the pass/fail column.
5. Coverage summary by function
| Function / module | Normal cases | Boundary cases | Invalid cases | Total cases | All passed? |
|---|---|---|---|---|---|
<<FILL>> | <<FILL: count>> | <<FILL: count>> | <<FILL: count>> | <<FILL>> | Yes/No |
<<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | <<FILL>> | Yes/No |
Any function with zero boundary or zero invalid cases is either genuinely free of decision logic (state why) or is under-tested; do not leave the gap unexplained.
6. Failures and discrepancies
Every failed case is investigated before the version proceeds. A failure is not “adjusted” by changing the expected value to match the actual output; that converts the record from evidence into narration.
| Test ID | Failure description | Root cause | Disposition | Corrective commit / fix | Re-tested (Test ID, date, result) |
|---|---|---|---|---|---|
<<FILL>> | <<FILL>> | <<FILL: code defect / DS defect / test-case error>> | <<FILL>> | <<FILL>> | <<FILL>> |
Where the root cause is a DS defect (the code is correct and the DS was wrong, ambiguous, or incomplete), the DS is corrected through change control before the record is closed, and the corrected DS is what future tests are written against.
7. Acceptance criteria for closing this record
- Every function and module in scope per section 2 has at least one normal, one boundary, and one invalid case, or the absence of a case type is justified.
- Every expected output names an independent source; none is derived from running the code under test.
- Every case shows an actual result and a pass/fail determination; “pass” with no recorded actual value does not satisfy this criterion.
- Every failure in section 6 is dispositioned, and any resulting code or DS change is re-tested before closure.
- The record is signed by the tester and reviewed by someone other than the author of the code under test.
8. References
21 CFR 211.68 (automatic, mechanical, and electronic equipment). 21 CFR Part 11 (electronic records and signatures). ISPE GAMP 5 (Second Edition), A Risk-Based Approach to Compliant GxP Computerized Systems, for the Category 5 unit testing expectation (reference by title; describe, do not paste). FDA guidance, Computer Software Assurance for Production and Quality Management System Software (current version).
Confirm the current version and clause numbers of each reference before issue.
9. Approvals
| Role | Name | Signature | Date |
|---|---|---|---|
| Tester | <<FILL>> | ||
| Independent reviewer (not the code’s author) | <<FILL>> | ||
| Validation lead / CSV | <<FILL>> | ||
| Quality Assurance | <<FILL>> |
10. Revision history
| Version | Date | Author | Summary of change |
|---|---|---|---|
<<FILL: 1.0>> | <<FILL: date>> | <<FILL: author>> | Initial issue. |
Filled specimen
The following shows the unit test log completed for the step-yield calculation script referenced in the Design Specification example (DS-CUST-014, algorithm ALG-001), version tagged v1.3.0, commit 9e21ab4. The company, system, and numbers are illustrative; replace them with your own.
| Test ID | Function | Case type | Test inputs | Expected output | Independent source | Actual output | Pass/Fail |
|---|---|---|---|---|---|---|---|
| UT-001 | calculate_step_yield() | Normal | load_titer=12.5, load_vol=400, pool_titer=8.2, pool_vol=550 | 90.2% (full precision 90.20) | Hand calculation, dated 15 Aug 2026, spreadsheet cross-check | 90.2% | Pass |
| UT-002 | calculate_step_yield() | Boundary, at low threshold | inputs chosen so yield = 70.0% exactly | result_flag = NORMAL (not LOW_YIELD, threshold triggers strictly below) | DS-CUST-014 section 6, step 6, hand-verified | result_flag = NORMAL | Pass |
| UT-003 | calculate_step_yield() | Boundary, just below low threshold | inputs chosen so yield = 69.9% | result_flag = LOW_YIELD | DS-CUST-014 section 6, hand-verified | result_flag = LOW_YIELD | Pass |
| UT-004 | calculate_step_yield() | Boundary, implausible ceiling | inputs chosen so yield = 150.1% | result_flag = IMPLAUSIBLE, result held pending reviewer acknowledgement | DS-CUST-014 section 6, hand-verified | result_flag = IMPLAUSIBLE, held | Pass |
| UT-005 | calculate_step_yield() | Invalid | load_titer=0, load_vol=400, pool_titer=8.2, pool_vol=550 | ERR-ALG-001 raised, no result calculated | DS-CUST-014 section 6.3, undefined-condition table | ERR-ALG-001 raised | Pass |
| UT-006 | calculate_step_yield() | Invalid | pool_vol = -50 (negative) | Rejected before calculation begins, input validation error | DS-CUST-014 section 5, field constraint (> 0) | Rejected at input validation | Pass |
Coverage summary: calculate_step_yield(), 1 normal, 3 boundary, 2 invalid, 6 total, all passed. No failures this cycle.
In this example, every expected output is traceable to either a hand calculation performed before the code was run, or to the DS’s own stated boundary and error-handling rules, worked out on paper against the inputs chosen to hit each condition. None of the six expected values was produced by running the script first and copying its output into the “expected” column; that is what makes this record evidence rather than a description of observed behavior.
Common inspection findings this record prevents
- Unit test records exist but show only “pass” with no recorded expected value, so there is nothing the actual result was checked against.
- The expected value in every test case was produced by running the code under test, making every test circular and incapable of failing.
- Testing covers only normal, typical inputs; the boundary and invalid cases most likely to reveal a defect were never exercised.
- A test fails, the expected value is quietly edited to match the code’s output, and the record is signed as a pass with no note that anything was adjusted.
- Unit testing was performed informally during development with no retained record, so there is no evidence it happened at all.
- The version tested cannot be tied to a specific commit or tag, so the test evidence cannot be matched to the version that was actually deployed.
How to adapt this record
- Set your record number, system name, and the DS and version this record tests against in the header.
- List every function or module in scope in section 2, and state why any function is excluded from unit testing.
- For each function, add a row per case in section 4, and name the independent source of every expected value; do not leave that column implicit.
- Complete the coverage summary in section 5 and confirm no function is missing a boundary or invalid case without a stated reason.
- Where a case fails, work it through section 6 to a disposition and a re-test before closing the record; never adjust the expected value to force a pass.
- Confirm every regulation in section 8 against the current published version before issue.