Paradigmi di Programmazione e Sviluppo — Prof. Viroli

PPS-01: Software Quality and Testing

Software Quality, Testing and Build Toolinga.a. 2025/2026

In this lesson

1. Good Design, Abstraction, and Process

Software accounts for a remarkably large share of the world economy — approximately 15 percent of global GDP indirectly, with estimates that the digital economy will reach 30 percent by 2030. Yet producing good software is costly: productivity is far below 100 lines of code per day per person when considering the whole lifecycle, from requirements through maintenance.

Key insight

Being able to write very high quality software means having a wide set of key skills — technical and organisational — the most important of which is a high ability in capturing and managing abstraction.

The need for a software process

Software development faces inherent challenges: software tends to be very complex, pieces of software are often easy to prototype but difficult to get right, and error-prone code can be extremely hard to debug. The time from prototype to correct, deployed software can be enormous. Without a disciplined process, software projects can and will simply fail.

Steps of software development

These steps are not strictly waterfall — modern processes iterate through them:

Frequently forgotten facts (R. Glass)

A 10% increase in problem complexity leads to 100% increase in solution complexity. Good programmers are up to 30 times better than mediocre ones. Error detection and removal accounts for 40% of development cost. Rigorous review removes 90% of errors. Maintenance consumes 40% to 80% of overall cost.

The abstraction principle

Abstraction is the single concept behind a variety of pragmatic concretions or individuals. In software engineering specifically, abstraction helps:

2. Software Quality Criteria

Definitions of software quality

There are multiple perspectives on what software quality means:

Internal vs external quality

External quality determines the fulfilment of stakeholder requirements: correctness, usability, efficiency, reliability, integrity, adaptability, accuracy, and robustness. Internal quality determines your ability to move forward on a project: maintainability, flexibility, portability, reusability, readability, testability, and understandability.

Oral exam tip

People typically perceive external quality, but internal quality is the key means to achieve external quality. Modern approaches require internal features to be readily observable. Aiming at external quality while bypassing internal quality is often the root cause of project failure. Be ready to explain this distinction.

Quality measurements

DimensionWhat it measures
ReliabilityResiliency to changes and structural solidity
EfficiencyGood performance and scalability
SecurityNo security breaches
MaintainabilityAdaptability, evolvability, control
SizeCodebase size metrics

3. Code Quality, Technical Debt, and Code Smells

Code quality as a precondition

Software quality is a combination of code, data, process, tools, platform, and library quality. Code quality is a necessary precondition to overall software quality. It focuses on the shape of code and how code can be evolved to achieve internal quality, which then impacts external quality.

Technical debt

Technical debt is the extra development work that arises when code that is easy to implement in the short run is used instead of applying the best overall solution. It is the "patch" problem: one quickly solves a problem sub-optimally, delaying improvement. Sooner or later, this costs more than fixing it immediately. Sometimes technical debt is necessary or unavoidable, but it must be managed consciously.

public class Configuration {
  private List<String> data;

  public void loadFromFile(final String fileName) {
    try {
      this.data = Files.readAllLines(Paths.get(fileName));
    } catch (IOException e) {
      System.out.println(e); // Technical debt here!
    }
  }

  public List<String> getData() {
    return Collections.unmodifiableList(this.data);
  }
}
Technical debt

The catch block above just prints the exception instead of properly handling it or propagating it. This is classic technical debt — quick to write now, but if the file is missing, the program silently continues with an empty data list, potentially causing hard-to-find failures later.

Code smells

Code smells are surface indications in source code that possibly indicate a deeper problem, violating fundamental design principles and negatively impacting quality. They are not necessarily bugs, but they typically create technical debt. Common examples include code duplication, bad names, abstraction jumps, partial implementations, and violation of simple OO principles.

Example 1: Neglecting code conventions

Every language has conventions beyond its syntax: style of names, spacing, formatting, obsolete constructs to avoid. In Java: never prefix interfaces with I, use camelCase, never declare two variables on the same line, do not abuse obscure operators.

Example 2: Not adhering to modern rules of effectiveness

Inspired by Effective Java by Joshua Bloch: know and use your libraries, minimise accessibility of class members, minimise mutability, favour composition over inheritance, prefer interfaces over abstract classes, program against interfaces not classes.

Example 3: Not using clean coding techniques

Code should be as readable as good prose, always reveal the intent, and be clear to all programmers regardless of skill level.

Meaningful names

Good functions and methods

4. Quality Framework: Code Defects

The course defines a comprehensive quality framework built on several layers: code defects, software engineering principles, OO design principles, design patterns, and process methodologies. The first layer — code defects — describes the problems that quality software must avoid.

The seven code defects

DefectMeaningConsequence
Needless repetitionCut-and-paste programming, duplicated logicBugs multiply from a single cause; fixing becomes impractical
Needless complexityOver-engineered solutions, reinventing wheelsCode is harder to understand, modify, and test
RigidityDesign is hard to changeSmall changes cause cascade modifications; reluctance to change
FragilityDesign is easy to breakChanges in one module break others unexpectedly
ImmobilityDesign is hard to reuseCode cannot be extracted without high cost and risk
ViscosityProper changes are costly; hacks are easierPsychological acceptance of low quality; pervasive technical debt
OpacityModules are difficult to understandHigh effort to keep intent understandable; abuse of comments

Needless repetition example

private Enemy createEnemy() {
  int type = rand.nextInt(2);
  if (type == 0) {
    return new Cactus(800, cactus1.getWidth() - 10,
            cactus1.getHeight() - 10, cactus1);
  } else {
    return new Cactus(800, cactus2.getWidth() - 10,
            cactus2.getHeight() - 10, cactus2);
  }
}

Needless complexity example

public class StatValuesImpl implements StatValues {
  private static final int NO_MAX = -1;
  private int actual;
  private int max;

  public void updateActual(int value) {
    if (actual + value <= 0) {
      actual = 0;
    } else {
      if (max != NO_MAX && actual + value > max) {
        actual = max;
      } else {
        actual = actual + value;
      }
    }
  }
}
Why this matters

The updateActual method mixes three concerns: clamping to zero, capping at a maximum, and normal addition. A simpler design would separate these concerns into distinct methods or use a well-known clamp pattern. Needless complexity like this makes code harder to test, reason about, and modify.

5. Quality Framework: Software Engineering Principles

Seven fundamental software engineering principles form the second layer of the quality framework. These principles guide how we structure code and processes to avoid the defects described above.

AbbrPrincipleMeaningExample
RFRigour and FormalityDescribe behaviour as precisely as mathematicsFinite state machines, DSLs, rule-like test specifications
SOCSeparation of ConcernsGroup issues, consider each in isolationDivide projects into increments, split unit/integration/acceptance tests
MODModularityDivide software into cohesive modules with low couplingPackage structure, nested classes, deployment modules
ABSAbstractionIdentify key aspects, ignore unnecessary detailsInterfaces, API layers, DSLs instead of APIs
AOCAnticipation of ChangeCreate ground for flexible reactivity to changesAbstract classes, constants, encapsulated implementation choices
GENGeneralitySolve a more general problem than the one at handParameters for implementation choices, supporting more input cases
INCIncrementalityMinimise risk by building in small incrementsVersioned releases, Scrum sprints, TDD Red-Green-Refactor cycles
Key idea

We all know these principles, and at least in principle concur they should be used. Sometimes we simply believe we have no time to follow them. In reality, one should judiciously know when to apply each one. The abstraction principle is arguably the single most important concept in software engineering — it is the key to managing complexity.

6. Quality Framework: OO Paradigm

Object-orientation is the dominant paradigm in mainstream software engineering. All top programming languages except C are OO. Most software engineering books apply to OO languages (Java, C#), and patterns like GoF design patterns, modelling languages like UML, and quality principles are all rooted in the OO world.

Key elements of OO

Key relationships in OO

Oral exam tip

Be able to explain the OO relationships with concrete examples. The Controller and Device example from the slides is the running case study throughout the quality and testing modules — make sure you understand how interfaces, implementations, and dependencies connect.

7. Software Methodologies and the Agile Manifesto

Critics of the waterfall model

The waterfall model (sequential phases: requirements, design, implementation, testing, maintenance) has three fundamental problems:

  1. Instability of requirements — requirements change, forcing a full cycle restart
  2. Difficulty in foreseen detailed design — many design choices emerge during coding and testing
  3. Problems with late testing — the longer you wait, the more code is built on top of buggy abstractions

The Agile Manifesto

The Agile Manifesto (2001), signed by the Agile Alliance, started from criticising the waterfall model and defined 12 principles. The key idea: do not refuse change, rather embrace it.

Principles that most affect coding

Agile programming as philosophy in this course

The course adopts an agile philosophy throughout: break development into short pieces and periods. In each period: (1) identify requirements, (2) identify architectural decisions, (3) write key interfaces, (4) write deep tests, (5) develop code until tests pass, (6) review/improve/refactor before merging. This promotes tools for continuous integration and emphasises people interactions over processes and tools.

8. Agile Unit Testing and Test-Driven Development

The Three Laws of TDD

  1. You may not write production code until you have written a failing unit test.
  2. You may not write more of a unit test than is sufficient to fail (and not compiling is failing).
  3. You may not write more production code than is sufficient to pass the currently failing test.

The TDD cycle: Red-Green-Refactor

The main methodology follows three steps in 10-20 minute cycles:

  1. Red — Write a test that fails
  2. Green — Get the test to pass (write the simplest production code)
  3. Refactor — Clean up any code added or changed in the prior two steps
Why be so strict?

Tests become a specification of intended behaviour, useful for understanding goals and documenting code. Tests dramatically reduce bug identification and resolution. The ability to quickly test gives freedom to refactor. Traditional cycle: Design, Code, Test. TDD cycle: Test, Code, Design/Refactor.

Kinds of tests

Roles of unit tests

Unit tests serve multiple purposes: regression prevention, progress measurement, specification, implementation guidance (as in TDD), solidity enforcement (edge cases), and legacy restoration (refactoring old code).

Clean tests

Test code is a system in itself and should be very clean. Key recipes: readability is paramount, use consistent naming conventions, adhere to build-operate-check pattern, strongly prefer one assert per test, single concept per test. Tests should be fast, independent, repeatable, self-validating, and timely.

Scala tests as "speakable specification"

Scala testing frameworks like ScalaTest allow tests to read like natural language specifications:

class EmptySetSpecification extends FlatSpec {
  "An empty Set" should "have size 0" in {
    Set.empty should have size (0)
  }

  it should "raise NoSuchElementException when head is called" in {
    an [NoSuchElementException] should be thrownBy (Set.empty.head)
  }
}

9. TDD Walkthrough: The DeviceManager Example

This section walks through the complete TDD example from the course slides: a Device and DeviceManager system. The interfaces are defined first, then tests drive the implementation.

public interface Device {
  void on();
  void off();
  boolean isOn();
  boolean isWorking();
}

A device can be switched on and off. It can also become non-working, meaning it is off and cannot be switched on again.

public interface DeviceManager {
  void addDevice(Device device);
  List<Device> devices();
  void switchAllOn();
  void switchAllOff();
  boolean allOn();
  boolean allWorking();
}

A manager that keeps track of devices and provides bulk operations: switching all on/off, checking status.

public class DeviceImpl implements Device {
  private boolean on = false;
  private boolean works = true;

  public void on() {
    if (this.isWorking()) { this.on = true; }
  }

  public void off() { this.on = false; }

  public boolean isOn() { return this.on; }

  public boolean isWorking() { return this.works; }

  public void stopWorking() {
    this.works = false;
    this.on = false;
  }
}
public class DeviceImplTest {
  private DeviceImpl device;

  @BeforeEach
  public void setUp() { this.device = new DeviceImpl(); }

  @Test public void isInitiallyOff() { assertFalse(device.isOn()); }

  @Test public void canBeSwitchedOn() { device.on(); assertTrue(device.isOn()); }

  @Test public void canBeSwitchedOff() { device.on(); device.off(); assertFalse(device.isOn()); }

  @Test public void onIsIdempotent() { device.on(); device.on(); assertTrue(device.isOn()); }

  @Test public void canStopWorking() {
    device.stopWorking();
    assertFalse(device.isWorking());
    device.on();
    assertFalse(device.isOn());
  }
}
public class DeviceManagerImpl implements DeviceManager {
  private List<Device> devices = new ArrayList<>();

  public void addDevice(Device device) { this.devices.add(device); }

  public List<Device> devices() { return Collections.unmodifiableList(this.devices); }

  public void switchAllOn() { this.devices.forEach(Device::on); }

  public void switchAllOff() { this.devices.forEach(Device::off); }

  public boolean allOn() {
    for (final Device device : this.devices()) { if (!device.isOn()) return false; }
    return true;
  }

  public boolean allWorking() {
    for (final Device device : this.devices()) { if (!device.isWorking()) return false; }
    return true;
  }
}
public class DeviceManagerImplTest {
  private DeviceManager manager;
  private List<Device> devices;

  @BeforeEach
  public void init() {
    this.manager = new DeviceManagerImpl();
    this.devices = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
      Device d = new DeviceImpl(); d.on();
      this.devices.add(d); this.manager.addDevice(d);
    }
  }

  @Test public void switchAllOn() {
    manager.devices().get(0).off();
    manager.devices().get(2).off();
    manager.switchAllOn();
    assertTrue(manager.allOn());
  }

  @Test public void someNotWorking() {
    DeviceImpl device = new DeviceImpl();
    device.stopWorking();
    manager.addDevice(device);
    assertFalse(manager.allWorking());
  }
}
Editor's note

The tests above follow the build-operate-check pattern: arrange (set up devices), act (call switchAllOn or add a broken device), assert (check allOn or allWorking). Each test focuses on a single concept. Note that devices() returns an unmodifiableList, enforcing immutability at the API boundary — a key quality practice from Effective Java.

10. Design Principles: DRY, KISS, and SOLID

DRY — Don't Repeat Yourself

Duplication is the quintessential code smell. When code similar to existing code is needed, the temptation is to cut, paste, and modify. This creates strong dependencies: errors propagate and evolve without control, and the system becomes extremely rigid. Duplication can be remedied by proper refactoring interventions.

// Before: repeated format logic
public class Logger {
  public static void logPrintString(final String s) {
    System.out.format("[Print] %s%n", s);
  }
  public static void logPrintStrings(final String[] array) {
    for (int i = 0; i < array.length; i++) {
      System.out.format("[Print] %s%n", array[i]);
    }
  }
}
// After: DRY applied, reusing the single-element version
public class LoggerDry {
  public static void logPrintString(final String s) {
    System.out.format("[Print] %s%n", s);
  }
  public static void logPrintStrings(final String[] array) {
    for (int i = 0; i < array.length; i++) {
      logPrintString(array[i]); // reused, no duplication
    }
  }
}

KISS — Keep It Simple, Stupid

"Most systems work best if they are kept simple rather than made complicated." Never use complex solutions when easier ones are possible. When a complex solution emerges, think twice.

// KISS: avoiding a tricky API, using straightforward println
public class LoggerKiss {
  public static void logPrintString(final String s) {
    System.out.println("[Print] " + s); // simpler than format
  }
  public static void logPrintStrings(final String[] array) {
    for (final String s : array) { // for-each instead of indexed loop
      logPrintString(s);
    }
  }
}

The SOLID Principles

The five SOLID principles are cornerstones of good design and programming, generalising OO:

SRP in action: moving responsibility away

public class LoggerSrp {
  public static void logPrintString(final String s) {
    System.out.println(PrefixFormatter.printPrefix() + " " + s);
  }
}

class PrefixFormatter {
  static String printPrefix() { return "[Print]"; }
}

DIP in action: depending on abstractions

public class LoggerDip {
  interface Console { void print(String s); void println(String s); }
  interface Formatter { String message(String s); }

  private final Formatter formatter;
  private final Console console;

  public LoggerDip(Formatter formatter, Console console) {
    this.formatter = formatter;
    this.console = console;
  }

  public void logPrintString(final String s) {
    console.print(formatter.message(s));
  }
}

11. Design Patterns for Quality

Design patterns capture experience on existing design to be reused as-is. They are not invented but extracted from analysing recurrent OO design solutions. The "Gang of Four" (GoF) book describes 23 patterns across three categories: creational, structural, and behavioural.

Why design patterns?

Patterns make your code more easily understandable by just recalling them by name. They make code more flexible and more open to change. They make your code much better organised. The course covers several key patterns in depth.

Strategy

A class includes an algorithm believed to easily change or come in different versions. The solution: capture the algorithm in an interface (Strategy), implemented by different classes. The main class takes a Strategy in its constructor or setter. This is a quintessential application of SRP and DIP. Example: java.util.Comparator / java.util.TreeSet.

// Strategy: filter devices by a predicate (functional strategy)
public List<Device> filteredDevices(Predicate<Device> predicate) {
  List<Device> list = new ArrayList<>();
  for (Device device : devices) {
    if (predicate.test(device)) { list.add(device); }
  }
  return Collections.unmodifiableList(list);
}

Template Method

A class includes an algorithm whose parts are expected to change. The solution: capture the variable algorithm parts as abstract methods, called by concrete (template) methods. A concrete subclass implements the abstract methods. Choice happens at compile-time rather than run-time. Example: java.io.InputStream.

public abstract class AbstractDevice implements Device {
  private boolean on;

  public final void off() {
    doWhenOff();   // template method calls abstract hook
    this.on = false;
  }

  protected abstract void doWhenOff(); // subclasses implement this
}

public class BrokenDeviceImpl extends AbstractDevice {
  protected void doWhenOff() {
    if (this.isOn() && Math.random() > 0.95) {
      this.stopWorking(); // 5% chance of breaking when turned off
    }
  }
}

Decorator

An interface has many implementations to be flexibly mixed. The solution: a decorator class implements the interface and accepts an element of the same kind, to which most method calls are redirected. A chain of decorations can be created at run-time. This is a mixture of strategy and template method. Example: java.util.stream.Stream.

// Decorator adds intensity control without modifying base Device
public class DeviceWithIntensityImpl implements Device {
  private final Device device;
  private int intensity;

  public DeviceWithIntensityImpl(Device device) {
    this.device = device;
  }

  public void increaseIntensity() { this.intensity++; }
  public int getIntensity() { return intensity; }

  // Delegating to the wrapped device
  public void on() { device.on(); }
  public void off() { device.off(); }
  public boolean isOn() { return device.isOn(); }
  public boolean isWorking() { return device.isWorking(); }
}

// Usage: compose decorators
Device d4 = new DeviceWithIntensityImpl(
            new BrokenDeviceImpl(
              new DeviceImpl("A")));

Proxy

Provide a surrogate (placeholder) for another object to control accesses to it. This separates and encapsulates access logic, keeps the base class clean, and can be made transparent to the client. Principles safeguarded: SRP (core behaviour vs access control), DIP (client depends on interface, enabling transparent proxy use). Examples: lazy initialisation, remote proxies, read-only collections.

Adapter

A class contains legacy behaviour that needs to be adapted to fit an interface someone expects. The solution: a thin adapter class implementing the target interface, delegating all needed work to the legacy class. Example: java.util.Arrays.asList.

// Adapter: connects a legacy Client expecting MyDeviceManager
// to the modern DeviceManager interface
public class MyDeviceManagerAdapter implements MyDeviceManager {
  final private DeviceManager manager;

  public MyDeviceManagerAdapter(DeviceManager manager) {
    this.manager = manager;
  }

  public void switchOn(int position) {
    this.manager.devices().get(position).on();
  }

  public void switchOff(int position) {
    this.manager.devices().get(position).off();
  }
}

Abstract Factory

The decision on which concrete classes to instantiate has to be taken dynamically. The solution: a factory interface with factory methods; actual implementations define which concrete classes to use. This makes code completely independent from actual implementation. Example: java.util.Stream.

public interface DeviceFactory {
  Device createStandardDevice(String name);
  Device createBrokenDevice(String name);
  Device createRandomDevice(String name);
}

public class DeviceFactoryImpl implements DeviceFactory {
  public Device createStandardDevice(String name) { return new DeviceImpl(name); }
  public Device createBrokenDevice(String name) { return new BrokenDeviceImpl(name); }
  public Device createRandomDevice(String name) {
    return Math.random() > 0.5 ? createStandardDevice(name) : createBrokenDevice(name);
  }
}

Builder

A class has too much construction logic with many inputs, default values, and sequential initialisation. The solution: a companion builder class whose sole responsibility is construction logic. A fluent interface is typically used for one-statement construction. Example: java.util.Stream.Builder, java.lang.StringBuilder.

public class DeviceImplBuilder {
  private String name = "Device";
  private String description = "";

  public DeviceImplBuilder setName(String name) {
    this.name = name; return this;
  }

  public DeviceImplBuilder setDescription(String description) {
    this.description = description; return this;
  }

  public DeviceImpl createDeviceImpl() {
    return new DeviceImpl(name, description);
  }
}

// Fluent usage:
Device d1 = new DeviceImplBuilder()
    .setName("A")
    .setDescription("a nice device")
    .createDeviceImpl();

12. Quality and Language Paradigms

Software quality is typically considered a problem only in big projects, which are mostly developed in OO mainstream languages. In reality, quality must be achieved independently of the language used — but having the proper language or paradigm can be a powerful tool to achieve quality because the right abstraction might be better captured.

The four main paradigms

ParadigmMottoConceptsExamples
ImperativeFirst do this, then do thatStatements, state change, procedures, modulesC, Pascal, Fortran
Object-OrientedSend a message to an objectObjects, classes, methods, fields, messagesJava, C++, C#
FunctionalEvaluate an expression and use the valueFunctions, no side effects, recursion, lambdasHaskell, Scala, ML, Scheme
LogicAnswer a computational question via searchRelations, unification, resolution, backtrackingProlog, Datalog

Declarative languages and quality

Declarative languages (functional and logic) express the logic of computation abstracting away from specific control flow — focusing on what rather than how. The functional paradigm hides the order of evaluation; the logic paradigm hides the search strategy. Advantages for quality: much more concise and elegant code; gives a feeling of actual computational steps emerging "magically."

Multi-paradigm approaches: Scala

Modern languages increasingly support multiple paradigms. Scala is a full OO language with a very advanced type system and first-order functions. This course uses Scala specifically because it combines OO and FP, making it a perfect training tool for both paradigms and their integration. Java and C# have also added primitive functional programming mechanisms (lambdas, streams).

Domain-specific languages

DSLs are languages specifically tailored to particular domains. They are often used in combination with mainstream languages. Turning an API into a DSL raises abstraction: "inside every system there's a small language struggling to get out." Examples include description languages for games, configurations, and software engineering.

Oral exam tip

The relationship between paradigm and quality is a common examination topic. Be ready to discuss how functional programming promotes immutability and referential transparency, how logic programming enables declarative specifications, and how Scala's multi-paradigm nature allows choosing the right abstraction for each subproblem.

13. Testing Deep Dive: Levels, Doubles, and Mockito

What is software testing?

Testing can be viewed from multiple angles: as an investigation providing information about quality, as executing a system under specified conditions to evaluate it, or as a lifecycle process to detect defects. There are different "schools" of testing: Analytic (formal methods), Factory (project management), Quality (process control), Context-Driven (stakeholder-oriented), and Agile (facilitating change).

Test doubles

To unit-test components that depend on others, we use test doubles — alternative implementations of depended-on components, quickly set up for testing with predictable behaviour. Test doubles must be free of errors "by construction."

TypePurposeHow it works
DummyReplace DOC with an untested reference"Empty object" passed to satisfy constructor parameters
StubPredefined responderEach method returns a fixed value
FakeLightweight version of the DOCReturns a sequence of results, simulating real behaviour
SpyProxy for a DOCTracks calls received, allows verification
MockCheck correct usage of DOCVerifies interaction patterns between SUT and DOC

Mockito

Mockito is a Java library for creating test doubles. Key constructs:

@Test
@DisplayName("Device must specify a strategy")
void testNonNullStrategy() {
  assertThrows(NullPointerException.class,
    () -> new StandardDevice(null));
}

@Test
@DisplayName("Device is initially off")
void testInitiallyOff() {
  // A dummy — unused reference, empty implementation
  FailingChecker dummy = mock(FailingChecker.class);
  device = new StandardDevice(dummy);
  assertFalse(device.isOn());
}
FailingChecker stubFailingChecker = mock(FailingChecker.class);

// Stubbing: define what the TD returns
when(stubFailingChecker.attemptOn()).thenReturn(false);
when(stubFailingChecker.checkerName()).thenReturn("mock");

assertThrows(IllegalStateException.class, () -> device.on());
assertEquals("StandardDevice{policy=mock, on=false}",
  device.toString());
FailingChecker fake = mock(FailingChecker.class);

// Faking: returns true, true, false in sequence
when(fake.attemptOn()).thenReturn(true, true, false);
when(fake.checkerName()).thenReturn("mock");

// First two calls succeed, third fails
device.on(); assertTrue(device.isOn()); device.off();
device.on(); assertTrue(device.isOn()); device.off();
assertThrows(IllegalStateException.class, () -> device.on());
FailingChecker spy = spy(new RandomChecker());
device = new StandardDevice(spy);

device.isOn();
verifyNoInteractions(spy); // no calls to DOC yet

try { device.on(); } catch (IllegalStateException e) {}
verify(spy).attemptOn(); // was attemptOn called?

device.reset();
assertEquals(2,
  Mockito.mockingDetails(spy).getInvocations().size());
FailingChecker mock = mock(FailingChecker.class);
when(mock.attemptOn()).thenReturn(true, true, false);
when(mock.checkerName()).thenReturn("mock");

verify(mock, times(0)).attemptOn(); // no calls yet
device.on();
verify(mock, times(1)).attemptOn(); // called once
assertTrue(device.isOn());
public class DeviceUnitTest {
  private Device device;
  @Mock FailingChecker checker;

  @BeforeEach
  void init() {
    MockitoAnnotations.openMocks(this);
    this.device = new StandardDevice(checker);
  }

  @Test void initiallyOff() {
    assertFalse(device.isOn());
    verifyNoInteractions(checker);
  }

  @Test void canBeSwitchedOn() {
    when(checker.attemptOn()).thenReturn(true);
    device.on();
    verify(checker).attemptOn();
    assertTrue(device.isOn());
  }

  @Test void switchOnMayFail() {
    when(checker.attemptOn()).thenReturn(false);
    assertThrows(IllegalStateException.class, () -> device.on());
    verify(checker).attemptOn();
  }
}

14. Integration Testing

Unit tests are not enough. We can never be sure a system works as a whole if we rely exclusively on unit tests. Integration tests verify how different parts of a system collaborate with each other and with external systems.

Key distinction

A unit test verifies a single unit of behaviour, does it quickly, and does it in isolation. If a test fails to meet any of these three requirements, it falls into the category of integration tests. Integration tests are not system tests — they carefully isolate the few collaborations needed to verify a multi-component behaviour.

Integration testing example

public interface DeviceManager {
  void start();
  void addDevice(String name, Device device);
  void switchOnAll();
  void switchOnOne(String name);
  boolean isSwitchedOn(String name);
}

public interface EventReporter {
  void reportStarted(String deviceName);
  void reportFailed(String deviceName, String reason);
}
public class IntegrationTest {
  private DeviceManager deviceManager;
  private Device device;
  @Mock FailingChecker checker;
  @Mock EventReporter reporter;

  @BeforeEach
  void init() {
    MockitoAnnotations.openMocks(this);
    this.deviceManager = Factory.createDeviceManager(reporter);
    this.device = spy(new StandardDevice(checker));
    this.deviceManager.addDevice("dev-0", this.device);
  }

  @Test
  void managerCorrectlyReportsADeviceFailure() {
    when(this.checker.attemptOn()).thenReturn(false);
    this.deviceManager.start();
    this.deviceManager.switchOnAll();

    assertFalse(this.deviceManager.isSwitchedOn("dev-0"));

    verify(this.device).on();
    verify(this.checker).attemptOn();
    verify(this.reporter).reportStarted("dev-0");
    verify(this.reporter).reportFailed(eq("dev-0"), anyString());
  }
}
Integration testing best practices

15. Check Your Understanding

1. Explain the difference between internal and external software quality. Why is internal quality important for achieving external quality?

External quality determines the fulfilment of stakeholder requirements — attributes like correctness, usability, efficiency, reliability. Internal quality determines your ability to move forward on a project — attributes like maintainability, flexibility, readability, testability. Internal quality is the key means to achieving external quality because you cannot sustainably produce correct, reliable software from a codebase that is rigid, fragile, opaque, or untestable. Modern approaches require internal features to be readily observable through clean code, tests, and documentation.

2. What is technical debt? Describe a concrete example from the slides.

Technical debt is the extra development work that arises when code easy to implement in the short run is used instead of the best overall solution. It is the "patch" problem: you solve something quickly and sub-optimally, delaying improvement until it costs more than fixing it immediately. In the slides, the Configuration class catches IOException and just prints it instead of properly handling or propagating it — classic technical debt that will cause silent failures later.

3. List the seven code defects described in the quality framework. Which one is considered the "quintessential code smell"?

The seven defects: (1) Needless repetition, (2) Needless complexity, (3) Rigidity, (4) Fragility, (5) Immobility, (6) Viscosity, (7) Opacity. Duplication (needless repetition) is the quintessential code smell — it creates strong dependencies where errors propagate without control and the system becomes extremely rigid.

4. What are the three laws of TDD? How does the TDD cycle differ from the traditional design-code-test sequence?

Law 1: No production code without a failing unit test. Law 2: No more of a unit test than is sufficient to fail. Law 3: No more production code than is sufficient to pass the failing test. Traditional: Design → Code → Test. TDD: Test (write a failing test) → Code (make it pass) → Design/Refactor (clean up). TDD inverts the order, making tests a specification tool and a safety net for refactoring.

5. Explain the five SOLID principles. Give a concrete code example for each.

SRP: One reason to change — separate PrefixFormatter from Logger. OCP: Open for extension, closed for modification — LoggerOcp uses a LogFormatter strategy. LSP: Subtypes must be substitutable — AbstractConsole enforces print/println semantics. ISP: Fine-grained client-specific interfaces — use Iterable<String> not String[]. DIP: Depend on abstractions, not concretions — LoggerDip depends on Console and Formatter interfaces, not System.out.

6. What is the difference between a Stub, a Spy, and a Mock in Mockito? When would you use each?

Stub: Provides predefined answers to method calls (when(...).thenReturn(...)). Use when you need a predictable response from a dependency. Spy: A proxy wrapping a real object to track calls (spy(new RandomChecker())). Use when you want to verify that methods were called but also keep real behaviour. Mock: An object used to verify interaction patterns (mock(FailingChecker.class) with verify(...).attemptOn()). Use when you need to check the exact number and order of calls between SUT and DOC.

7. How does the Decorator pattern differ from the Template Method pattern? When would you choose one over the other?

Template Method uses inheritance and abstract methods to let subclasses define variable parts of an algorithm — the variation point is decided at compile-time. Example: AbstractDevice.doWhenOff(). Decorator uses composition and delegation to wrap objects with additional behaviour — multiple decorators can be chained at run-time. Example: wrapping a DeviceImpl with BrokenDeviceImpl and DeviceWithIntensityImpl. Choose Template Method when the variation is fixed at subclasses; choose Decorator when you need to mix behaviours flexibly at run-time.

8. Describe the Agile Manifesto's key principles that most affect coding. How does agility address the problems of the waterfall model?

Key principles: satisfy customers through early/continuous delivery; welcome changing requirements; deliver working software frequently; working software is the primary measure of progress; continuous attention to technical excellence; simplicity is essential. Agile addresses waterfall's three problems: (1) requirements instability is embraced through iterative cycles, (2) detailed design emerges through coding and testing in short iterations, (3) testing is continuous, not deferred to the end — preventing late-discovered bugs.

9. What is the Abstraction principle and why is it considered the single most important concept in software engineering?

Abstraction is the process of identifying common features in individuals and forming a concept of those features, ignoring unnecessary details. In software engineering, it mitigates opacity (code directly captures the idea), enforces DRY (don't repeat yourself), and enables DIP (depend on abstractions, not details). It is considered most important because managing complexity in software — the single biggest challenge — fundamentally relies on creating the right abstractions at every level: classes, interfaces, packages, APIs, DSLs, and architectural layers.

10. Why is the Builder pattern useful? Provide a scenario from the course examples where it applies.

Builder is useful when a class has too much construction logic with many optional parameters, default values, or sequential initialisation steps — precisely the situation where telescoping constructors become unreadable and error-prone. In the course, DeviceImpl had multiple constructors for different combinations of name and description. The DeviceImplBuilder replaces those with a fluent API: new DeviceImplBuilder().setName("A").setDescription("nice device").createDeviceImpl(). This applies SRP by separating construction logic from the class itself.