Code Quality, Testing and DebuggingIntroductionSoftware that merely runs is not necessarily software that is correct, maintainable, secure, or suitable for production. Code quality is the collection of properties that make software dependable and economical to change. In Java and Python projects, quality is achieved through consistent conventions, clear design, automated verification, informative diagnostics, disciplined debugging, and constructive review. Testing and debugging are related but different. Testing detects failures or provides evidence that specified behaviour is present. Debugging investigates why an observed failure occurred and changes the system so that the underlying defect is removed. A strong development process does not treat these activities as a final inspection stage; it integrates them throughout the software life cycle. This chapter considers practical techniques suitable for diploma-level project development. Examples use Java and Python, but the principles apply to most programming languages. Learning ObjectivesBy the end of this chapter, you should be able to: - apply coding conventions and documentation practices in Java and Python;
- distinguish static analysis, testing, logging, debugging, and code review;
- design effective unit, integration, and end-to-end tests;
- use assertions, fixtures, test doubles, and mocks appropriately;
- automate tests and quality checks in a build or continuous-integration pipeline;
- interpret failures, isolate root causes, and use debuggers and logs systematically;
- refactor code without changing its externally observable behaviour;
- evaluate quality practices in terms of reliability, maintainability, security, and professional responsibility.
1. A Practical Model of Code QualityQuality is multidimensional. Important attributes include: | Attribute | Meaning | Typical evidence |
|---|
| Correctness | The program satisfies its requirements | Passing functional tests, accepted outputs | | Readability | A developer can understand the code | Clear names, simple control flow | | Maintainability | Changes can be made safely and economically | Modular design, low duplication, tests | | Reliability | The system behaves consistently under expected conditions | Integration tests, monitoring, fault handling | | Security | The system resists misuse and protects data | Validation, secure dependencies, access control | | Performance | The system uses acceptable time and resources | Benchmarks and performance tests |
No single metric proves quality. For example, high test coverage does not guarantee that the tests check meaningful behaviour. Similarly, a short program may be difficult to maintain if it hides complex assumptions. A useful professional principle is that quality is designed in rather than inspected in. Requirements should be precise, interfaces should have clear contracts, and automated checks should run frequently. 1.1 Coding conventionsConventions reduce unnecessary decisions and make defects easier to notice. A team should agree on naming, formatting, file organisation, error handling, comments, and use of language features. In Java, common conventions include: - classes use
PascalCase, such as PaymentService; - methods and variables use
camelCase, such as calculateTotal; - constants use uppercase with underscores, such as
MAX_RETRIES; - packages use lowercase names;
- braces and indentation follow the project formatter;
- fields are normally private and accessed through a deliberate interface.
In Python, the widely used PEP 8 style recommends: snake_case for functions and variables;PascalCase for classes;- uppercase names for constants;
- four spaces for indentation;
- short, focused functions and imports grouped consistently.
Automatic formatters such as Google Java Format, Checkstyle-compatible formatters, Black, and Ruff reduce debates about layout. A formatter should be applied consistently, preferably by a build or pre-commit process rather than manually by selected developers. 1.2 Documentation and contractsComments should explain intent, constraints, or non-obvious decisions—not repeat the syntax. Good documentation answers questions such as: - What does this public method do?
- What inputs are valid?
- What does it return?
- Which exceptions can occur?
- What side effects or thread-safety assumptions exist?
Java commonly uses Javadoc: /**
* Converts an amount from cents to the requested currency scale.
* @param cents non-negative amount in cents
* @return amount represented in major currency units
* @throws IllegalArgumentException if cents is negative
*/
public BigDecimal toMajorUnits(long cents) { ... }Python commonly uses docstrings and type hints: def discount(price: float, rate: float) -> float:
"""Return price after applying a rate between 0 and 1."""A docstring is not a substitute for validation. If rate must be between zero and one, the implementation should enforce that contract or clearly document that the caller is responsible. 2. Static Analysis and Design ImprovementStatic analysis examines source code without executing the program. Compilers, type checkers, linters, security scanners, and complexity tools identify possible errors early. Typical checks include unreachable code, unused variables, suspicious comparisons, unsafe dependencies, missing exception handling, excessive complexity, and inconsistent style. Java projects may use the compiler, Checkstyle, PMD, SpotBugs, and IDE inspections. Python projects may use Ruff, Pylint, Flake8, mypy, Pyright, Bandit, and IDE inspections. Static analysis produces warnings, not infallible truths. A warning should be understood and either corrected or consciously suppressed with a documented reason. Suppressing all warnings destroys the value of the tool. 2.1 Complexity and maintainabilityCyclomatic complexity is a structural measure related to the number of independent paths through a method. One common formulation is: M = E − N + 2P Here, M is cyclomatic complexity, E is the number of control-flow edges, N is the number of nodes, and P is the number of connected components, normally 1 for a single method. In a simplified method graph, each decision point generally increases the value by one. The measure is useful for identifying code that may need decomposition, but it does not measure correctness or human difficulty directly. For example, a method with three independent if or loop decisions often has a complexity near four, assuming one connected component. Such a method may require several carefully chosen tests, but the exact test count depends on the conditions and their interactions. 2.2 RefactoringRefactoring changes the internal structure of code while preserving its externally observable behaviour. Common refactorings include extracting a method, renaming a variable, replacing duplicated logic with a shared function, introducing a value object, and simplifying conditional logic. A safe refactoring cycle is: - establish a passing baseline with tests;
- make one small structural change;
- run unit tests, static analysis, and formatting checks;
- inspect the resulting design;
- commit a focused change.
Refactoring is not the same as adding a feature or rewriting a system. A large rewrite removes the safety of small, verifiable steps. Refactoring should also avoid premature abstraction: duplicated code is sometimes clearer than a generalisation that has not yet proved useful. 3. Logging and ObservabilityA production system needs evidence about what it did. Logging records significant events, while observability combines logs with metrics and traces to explain system behaviour from the outside. A useful log entry normally contains a timestamp, severity, component, event message, and relevant correlation or request identifier. Log levels commonly include DEBUG, INFO, WARNING or WARN, ERROR, and CRITICAL or FATAL. Levels should reflect operational significance rather than a developer's personal preference. Avoid logging passwords, authentication tokens, full payment-card numbers, health information, or unnecessary personal data. Use structured logging where possible, for example JSON fields rather than strings that must later be parsed. Parameterised logging also avoids unnecessary message construction: logger.info("order accepted", extra={"order_id": order_id, "total": total})Logging is not a substitute for error handling. An exception should be handled at a layer that can take useful action, and it should not be logged repeatedly at every layer. In distributed applications, correlation identifiers help connect events belonging to one request. 4. Testing as Evidence of BehaviourA test compares actual behaviour with an expected result under defined conditions. A well-designed test has a clear purpose and is independent, repeatable, deterministic, and easy to diagnose. 4.1 The test pyramidThe test pyramid is a planning model rather than a strict law: - Unit tests are numerous and fast. They isolate a function, method, or small class.
- Integration tests verify cooperation between components, such as an application and a database.
- End-to-end tests exercise a complete user or business workflow and are usually fewer because they are slower and more fragile.
A project that has only end-to-end tests may detect failures late and provide poor fault localisation. A project with only unit tests may miss configuration, database, network, and interface defects. 4.2 Test designThe Arrange–Act–Assert pattern gives a test a readable structure: - Arrange the inputs, dependencies, and initial state.
- Act by calling the behaviour under test.
- Assert the observable result or interaction.
A test should verify behaviour rather than implementation details. For example, a test for a shopping basket should assert the total and perhaps the published event—not that a private helper was called twice. Test cases should include normal values, boundary values, invalid values, and relevant combinations. Boundary testing is especially important for empty collections, zero, maximum allowed values, dates at a changeover, and text length limits. A test oracle is the basis for deciding whether an outcome is correct. It may be a requirement, a known calculation, an invariant, a reference implementation, or a contract. Without a trustworthy oracle, a passing test may simply confirm the program's existing mistake. 4.3 Unit testing in Java and PythonJava projects commonly use JUnit 5. Python projects commonly use unittest or pytest. The following examples express the same rule: a percentage discount must reduce a valid price and reject an invalid rate. @Test
void appliesDiscount() {
assertEquals(new BigDecimal("90.00"),
pricing.discount(new BigDecimal("100.00"), new BigDecimal("0.10")));
}def test_applies_discount():
assert discount(100.0, 0.10) == 90.0For monetary software, binary floating-point values can introduce representation and rounding issues. Java BigDecimal or Python's Decimal is generally more appropriate when exact decimal rules are required. Tests should specify the rounding policy rather than relying on accidental behaviour. Assertions should be specific. An assertion that an object is merely non-null provides less evidence than one that verifies its status, identifier, and relevant values. Tests should also verify expected exceptions: def test_rejects_rate_outside_range():
with pytest.raises(ValueError):
discount(100.0, 1.2)4.4 Fixtures and test isolationA fixture is the known setup required by a test. It may be created before a test and cleaned afterward. Fixtures should be minimal. Shared mutable fixtures can cause order-dependent failures, in which one test changes state that affects another. Each test should ideally be able to run alone and in any order. Randomised tests can be valuable, but failures must be reproducible by recording the random seed and input. Time, file systems, networks, and environment variables are common sources of nondeterminism; they should be controlled through dependency injection or test-specific substitutes. 5. Test Doubles and MockingA test double replaces a real dependency during a test. The terms are often used as follows: - stub: supplies predetermined data;
- fake: a simpler working implementation, such as an in-memory repository;
- spy: records calls for later inspection;
- mock: verifies expected interactions and may provide responses.
Mocking is useful when a dependency is slow, expensive, unavailable, or has side effects. It is harmful when tests assert every internal call and therefore break during harmless refactoring. Mock the boundary that your code owns or relies on, not every object in the call chain. Dependency injection makes substitution explicit: class OrderService {
private final PaymentGateway gateway;
OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}The service can receive a real gateway in production and a fake or mock gateway in a unit test. Python often uses constructor parameters, function arguments, or unittest.mock.patch, but patching should target the name used by the module under test. A useful rule is to test state when the result is the main concern and interactions when an interaction is itself part of the contract—for example, ensuring a payment is not charged twice. 6. Integration, Regression, and AutomationIntegration tests verify real collaboration. Examples include testing a repository against a temporary database, an HTTP client against a test server, or a Java service with its configured dependency container. These tests reveal schema mismatches, serialisation errors, incorrect configuration, transaction problems, and incompatible versions. A regression test is added after a defect is discovered so that the defect does not return. The most valuable regression test reproduces the original failure before the fix and passes after the fix. A practical automated pipeline may perform the following sequence: - install a locked dependency set;
- compile or type-check the project;
- format-check and run linters;
- run unit tests;
- run integration tests using isolated services;
- generate coverage and test reports;
- scan dependencies and source for security issues;
- package the application only if required checks pass.
Continuous integration provides rapid feedback for each change. A failed pipeline should be visible and assigned; permanently ignored failures create false confidence. Test coverage is useful as a diagnostic indicator, but a coverage percentage is not a quality target by itself. Branches and error paths that matter may remain untested even when line coverage is high. 7. Debugging SystematicallyDebugging should be treated as an evidence-based investigation rather than random editing. A disciplined process is: - reproduce the failure reliably;
- record the input, environment, expected result, and actual result;
- classify the symptom and locate the narrowest failing boundary;
- inspect stack traces, logs, recent changes, and relevant state;
- form a hypothesis;
- test the hypothesis with a controlled experiment;
- implement the smallest appropriate fix;
- add or improve a regression test;
- rerun broader tests and document the cause.
A stack trace identifies the call path at an exception, but the deepest frame is not automatically the root cause. The first application frame and the data passed into it often provide more useful evidence. Interactive debuggers support breakpoints, stepping, watch expressions, call-stack inspection, and conditional stops. They are valuable for understanding state transitions, but excessive stepping can obscure the larger design. Logging and a minimal failing test often provide a more repeatable investigation. Common causes include incorrect assumptions about null or None, off-by-one boundaries, shared mutable state, resource leaks, race conditions, timezone mistakes, encoding errors, stale caches, and exception handling that hides the original problem. A fix should address the cause, not merely suppress the symptom. 8. Code Review and Professional PracticeCode review is a technical quality activity and a knowledge-sharing activity. Reviewers should examine correctness, requirements, error handling, security, performance risks, tests, readability, and maintainability. Authors should provide a focused change, explain important decisions, and identify known limitations. A productive review distinguishes blocking defects from suggestions. Review comments should be specific and respectful: describe the risk, explain why it matters, and where possible propose an alternative. Automated formatting and linting should handle mechanical issues so that human attention can focus on design and behaviour. A reviewer can use this sequence: - What requirement does this change implement?
- What happens for invalid, empty, or extreme input?
- Are failures observable and safely handled?
- Are tests independent, meaningful, and sufficient for the risk?
- Does the change introduce security, privacy, concurrency, or performance concerns?
- Is the interface clearer or more complex than necessary?
Review is not a substitute for testing, and approval does not transfer responsibility from the author. Both author and reviewer share responsibility for the quality of the merged result. 9. Applied Case: Reliable Order ProcessingThe following is an illustrative case. A small online retailer has a Python order service and a Java inventory service. Customers report that some orders are charged twice, while support staff cannot determine which inventory operation failed. Background and problemThe Python service receives a checkout request, calculates a total, calls the payment provider, and sends an order message. A retry mechanism repeats the whole operation when a network timeout occurs. The Java service consumes the message and decrements inventory. Logs contain free-text messages without an order identifier. The symptoms suggest at least two risks: the payment operation is not idempotent, and the workflow lacks a traceable correlation identifier. The issue cannot be solved reliably by adding a single conditional because a timeout does not reveal whether the payment provider completed the charge. AnalysisThe team first writes a regression test for a retry after an ambiguous payment response. It verifies that the same payment request identifier is reused and that the customer is charged at most once according to the provider's idempotency contract. A unit test uses a fake payment gateway that returns a timeout after recording the request. An integration test then uses the provider's sandbox or a contract-compatible test server. The team also adds tests for invalid totals, duplicate messages, insufficient stock, and payment success followed by message-publication failure. These tests expose that inventory consumption is not idempotent either: the same message can decrement stock twice. Approach and solutionThe service generates a stable order_id and payment_request_id before the first external call. Retries reuse the payment identifier. The Java consumer stores processed message identifiers or uses a transactional mechanism so that duplicate delivery does not repeat the business effect. Both services emit structured logs containing order_id, operation, outcome, and duration, while excluding payment secrets. The code is refactored so calculation, payment coordination, and message publication are separate components. Unit tests cover calculation and decision logic; integration tests cover the database, message broker, and provider adapter. Static analysis is added to the CI pipeline, and a review checklist requires tests for retries and duplicate delivery. Outcome and learning pointsThe design does not pretend that networks are reliable. Instead, it makes retries safe, makes ambiguous outcomes observable, and tests the boundaries where failure is most likely. The key lessons are: - external side effects require explicit idempotency considerations;
- a stable identifier improves both correctness and diagnosis;
- unit tests alone cannot prove that configured components integrate correctly;
- logs should support investigation without exposing confidential data;
- regression tests preserve knowledge of expensive failures.
10. Limitations, Ethics, and Contemporary PracticeQuality tools have limitations. Static analysers produce false positives and false negatives. Mocks may model an external service inaccurately. Integration tests may pass in a controlled environment while production configuration differs. Automated tests can encode obsolete requirements. Human review and operational feedback remain necessary. Testing must also respect privacy and security. Production personal data should not be copied casually into test environments. Test data should be synthetic or minimised, access should be controlled, and logs should be reviewed for sensitive information. Security testing should include dependency scanning, input validation, authentication and authorisation checks, and tests for injection and unsafe deserialisation where relevant. AI-assisted coding can generate tests, explanations, or refactoring suggestions, but generated code and tests require human review. A generated test may assert an implementation detail, omit important edge cases, reproduce the same misunderstanding as the production code, or introduce insecure dependencies. Teams should preserve authorship, review licensing implications, and avoid sending confidential source or personal data to unapproved services. Modern projects increasingly combine unit and integration tests with contract testing, mutation testing, property-based testing, and observability. Contract tests check that independently deployed components agree on an interface. Mutation testing deliberately introduces small code changes to assess whether tests detect them. Property-based testing generates many inputs from general rules, such as the property that sorting preserves collection size and members. These techniques complement, rather than replace, clear requirements and ordinary example-based tests. Best-Practice Checklist- Use a shared formatter and documented naming conventions.
- Keep functions focused and make contracts explicit.
- Treat compiler, linter, type-checker, and security warnings seriously.
- Write small tests around observable behaviour.
- Include invalid and boundary cases.
- Isolate external resources and control nondeterminism.
- Use mocks sparingly and prefer simple fakes where appropriate.
- Add regression tests for every significant defect.
- Automate quality checks in continuous integration.
- Log useful context, but never expose secrets unnecessarily.
- Debug from reproducible evidence and verify the root cause.
- Keep reviews focused, respectful, and risk-oriented.
- Refactor in small steps with a passing test baseline.
Chapter SummaryCode quality is a combination of correctness, readability, maintainability, reliability, security, and suitable performance. Coding conventions, documentation, static analysis, and refactoring reduce avoidable complexity. Testing supplies evidence of behaviour: unit tests are fast and focused, integration tests verify collaboration, and end-to-end tests validate complete workflows. Fixtures, dependency injection, and carefully selected test doubles improve isolation, while regression tests protect against recurring defects. Debugging is a structured investigation based on reproducible evidence, hypotheses, and verification. Logging and observability make failures diagnosable, and code review provides human evaluation of behaviour and design. Continuous integration turns these practices into repeatable controls. The illustrative order-processing case shows why retries, idempotency, structured logs, and boundary-focused tests are essential in distributed systems. Finally, tools—including AI tools—assist professional judgement but cannot replace requirements, ethical handling of data, or critical review. Key TermsCode quality: The degree to which software satisfies requirements and remains dependable and maintainable. Coding convention: An agreed rule for naming, formatting, organisation, or implementation style. Static analysis: Examination of source code without executing it. Linter: A tool that detects style problems and possible defects. Refactoring: Restructuring code while preserving externally observable behaviour. Logging: Recording significant software events for diagnosis and operations. Unit test: A test of a small isolated unit of behaviour. Integration test: A test of collaboration between components or external resources. End-to-end test: A test of a complete workflow through the system. Regression test: A test that prevents a previously fixed defect from returning. Fixture: Prepared state or data used by a test. Test double: A substitute used in place of a real dependency during testing. Mock: A test double that can verify expected interactions. Test oracle: The basis used to decide whether an outcome is correct. Code coverage: A measure of which code elements were executed by tests. Cyclomatic complexity: A control-flow measure associated with independent paths. Continuous integration: Frequent automated building and verification of changes. Idempotency: The property that repeating an operation has the same intended effect as performing it once. Correlation identifier: An identifier used to connect events belonging to one request or workflow. Observability: The ability to infer internal system behaviour from outputs such as logs, metrics, and traces. Review QuestionsShort Answer Questions- Distinguish testing from debugging.
- State two differences between a unit test and an integration test.
- Why should logging avoid passwords and authentication tokens?
- What is the purpose of a regression test?
- Name three common types of test double.
Long Answer Questions- Explain how coding conventions, static analysis, documentation, and refactoring collectively improve maintainability.
- Describe the Arrange–Act–Assert pattern and explain how it supports readable tests.
- Compare unit, integration, and end-to-end testing, including the strengths and limitations of each.
- Present a systematic debugging process for a defect that cannot initially be reproduced reliably.
- Explain how continuous integration can combine formatting, static analysis, tests, coverage reporting, and security checks.
Analytical / Application Questions- A payment service retries after a network timeout and customers are sometimes charged twice. Analyse the likely cause and propose tests and design changes to make the workflow safer.
- A Python module has high line coverage, but production failures occur for empty input, timezone boundaries, and duplicate messages. Evaluate why coverage was insufficient and design a stronger test strategy.
- During code review, a developer proposes mocking every internal method of a service. Analyse the risks of this approach and recommend a more robust testing design.
Multiple Choice Questions with AnswersQuestion 1Which activity examines source code without executing the program? A. Static analysis B. End-to-end testing C. Interactive debugging D. Load testing Correct Answer: A. Static analysis Question 2What is the main purpose of refactoring? A. To add new externally visible features B. To change internal structure while preserving observable behaviour C. To remove all automated tests D. To replace integration tests with comments Correct Answer: B. To change internal structure while preserving observable behaviour Question 3In the Arrange–Act–Assert pattern, what happens during Act? A. Test data and dependencies are prepared B. The expected result is documented C. The behaviour under test is executed D. The test report is archived Correct Answer: C. The behaviour under test is executed Question 4Which test double is primarily intended to provide predetermined data? A. Stub B. Spy C. Mock D. Production adapter Correct Answer: A. Stub Question 5Which practice best reduces order-dependent test failures? A. Sharing mutable state across all tests B. Allowing tests to depend on execution order C. Giving each test isolated setup and cleanup D. Disabling tests that fail intermittently Correct Answer: C. Giving each test isolated setup and cleanup Question 6Why is idempotency important in a retryable payment operation? A. It guarantees that the network never fails B. It prevents a repeated request from creating an unintended duplicate charge C. It removes the need for authentication D. It makes all payments free of rounding rules Correct Answer: B. It prevents a repeated request from creating an unintended duplicate charge Question 7Which statement about code coverage is correct? A. High coverage proves that requirements are correct B. Coverage measures whether tests are meaningful C. Coverage indicates executed code but does not by itself prove test quality D. Coverage makes integration tests unnecessary Correct Answer: C. Coverage indicates executed code but does not by itself prove test quality Question 8Which logging practice is most appropriate for production diagnostics? A. Log authentication tokens for troubleshooting B. Use structured context such as a correlation identifier while protecting sensitive data C. Log every internal variable at error level D. Suppress all exceptions after logging a generic message Correct Answer: B. Use structured context such as a correlation identifier while protecting sensitive data Question 9What is the first useful step when debugging a reported defect? A. Rewrite the entire application B. Reproduce and clearly record the failure C. Delete the failing test D. Suppress the exception Correct Answer: B. Reproduce and clearly record the failure Question 10Which test level is most directly suited to verifying that an application works correctly with a database? A. Unit testing with no database substitute B. Integration testing C. Formatting validation D. Code review alone Correct Answer: B. Integration testing
|