Test Double: a Spy

medium Testing and Clean Code

Problem

A NotificationService must send a message through a Notifier, unless the user is muted. In a test, you do not want to send real messages, so you give the service a spy: a Notifier that only records the calls. Commands: mute user, unmute user, notify user message (the message is the rest of the line and can have spaces; no output) and report. report prints calls=N, where N is the number of messages that reached the notifier so far, followed by one line per message: user: message.

Constraints

  • 1 <= commands <= 10^4

Examples

Input

notify amit hi
mute amit
notify amit no
report

Output

calls=1
amit: hi

Solutions

Best approachTime O(n)Space O(n)

A spy records how it was used, so a test can check the behaviour afterwards: how many calls, with which values. A stub only returns fixed answers; a mock also checks the expectations itself; a fake is a light working copy (an in-memory database). Mockito (Java), Jest (JavaScript) and unittest.mock (Python) create these for you.

Same code works in Java810172125Basic features only, so nothing changes between versions.
/*
 * Problem : Process lines.
 * Input   : Several lines.
 * Output  : Spy reports.
 * Approach: Service with a spy.
 * Time    : O(n)
 * Space   : O(n)
 * Java    : 8 or newer.
 */
import java.util.Scanner;

public class Main {
        // Step 1: The service depends on this interface. A real notifier would send an e-mail or an SMS.
        interface Notifier {
            void send(String user, String message);
        }

        // Step 2: The spy is a test double. It sends nothing and only records every call.
        static class SpyNotifier implements Notifier {
            final java.util.List<String> calls = new java.util.ArrayList<>();

            @Override
            public void send(String user, String message) {
                calls.add(user + ": " + message);
            }
        }

        // Step 3: The service holds the rule "muted users get nothing". It does not know which notifier it has.
        static class NotificationService {
            private final Notifier notifier;
            private final java.util.Set<String> muted = new java.util.HashSet<>();

            NotificationService(Notifier notifier) {
                this.notifier = notifier;
            }

            void mute(String user) {
                muted.add(user);
            }

            void unmute(String user) {
                muted.remove(user);
            }

            void notify(String user, String message) {
                if (!muted.contains(user)) {
                    notifier.send(user, message);
                }
            }
        }

    // This method solves the task. It receives the input and returns the answer.
    static String[] solve(String[] lines) {
        SpyNotifier spy = new SpyNotifier();
        NotificationService service = new NotificationService(spy);
        java.util.List<String> output = new java.util.ArrayList<>();
        for (String line : lines) {
            if (line.trim().isEmpty()) {
                continue;
            }
            String[] part = line.split(" ", 3);
            if (part[0].equals("mute")) {
                service.mute(part[1]);
            } else if (part[0].equals("unmute")) {
                service.unmute(part[1]);
            } else if (part[0].equals("notify")) {
                service.notify(part[1], part[2]);
            } else {
                // Step 4: The "test" asks the spy what happened.
                output.add("calls=" + spy.calls.size());
                output.addAll(spy.calls);
            }
        }
        return output.toArray(new String[0]);
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        java.util.List<String> all = new java.util.ArrayList<>();
        while (scanner.hasNextLine()) {
            all.add(scanner.nextLine());
        }
        String[] lines = all.toArray(new String[0]);
        System.out.println(String.join("\n", solve(lines)));
    }
}
Hints (1)
  1. What is the difference between a stub and a spy?