DEV Community

Cover image for Character Copy Kata ( Test Driven Development )
Chiranjeev Thomas
Chiranjeev Thomas

Posted on

Character Copy Kata ( Test Driven Development )

A great kata for introducing jest mocks functions in your project.


Try attempting to solve the kata yourself before looking at the solution : Click here to view the Kata


This Kata expects you to create a character copier class that reads characters from a source and copies them to a destination. The class must copy and write one character at a time

To do this, you need to create a Copier class that takes in a Source and a Destination

The Source interface has a method
char ReadChar() and the Destination interface has a method void WriteChar(char c). The Copier class has a method void Copy() that reads from the ISource one character at a time and writes to the IDestination until a newline (\n) is encountered.

@ The copying and writing is done 1 character at a time.

@ Only the Copier class may exist as a concrete class.

@ You can use any language to implement the Source and Destination interfaces


We're going to solve the following problem using Typescript and the Jest testing framework

First, lets identify all the possible tests you could think of for this scenario

[ ❓ ] No characters , ending in newline

[ ❓ ] Once character , ending in newline

[ ❓ ] Two characters , ending in newline

[ ❓ ] Many characters, ending in newline

[ ❓ ] Order of characters copied should be maintained

[ ❓ ] Characters after newline should not be written


Case 1 : [❓] No characters , ending in newline

Step 1 : Define the most basic test case scenario


//copy.test.ts

  describe('when no character is read', () => {


    it('does not call the desination interface`s writeChar method ', () => {
      const src: Source = getSource([]); // helper method

      const dest = getDestination(); // helper method

      const sut = new Copier(src, dest);

      sut.copy();

      expect(dest.writeChar).toBeCalledTimes(0);
    });
  });

Enter fullscreen mode Exit fullscreen mode

Step 2 : Defining the helper functions

To write tests, we will need two helper methods called getSource and getDestination
that will provide us with the Source and Destination interfaces.

This will allow us to avoid repeatedly creating them for each test, and also make it easier to change any implementation details if needed

( p.s : This step can come at a later stage too, after the tests become repetative.
But in our case, we'll be defining them from the very beginning )


//copier.helper.ts

import { Source } from './character-copier';

/* The code is creating a mock function `mockCharReader` using `jest.fn()`. This mock function will be
used to simulate the behavior of a character reader object. */

export const getSource = (elements: string[]): Source => {

  const mockCharReader = jest.fn();

  elements.forEach((e) => mockCharReader.mockReturnValueOnce(e));

  /* The line `mockCharReader.mockReturnValue('\n');` is setting the return value of the `mockCharReader`
function to be the newline character (`'\n'`). This means that when `readChar` is called on the mock
character reader object, it will return `'\n'` as the next character from the source. */

  mockCharReader.mockReturnValue('\n');

  /* The `return` statement is creating an object with a property `readChar` that is assigned the value
  of the `mockCharReader` function. This object is then returned by the `getSource` function. */

  return {
    readChar: mockCharReader,
  };
};

Enter fullscreen mode Exit fullscreen mode


//copier.helper.ts

/* The code is defining a function named `getDestination` that returns an object with two properties:
`writeChar` and `getWrittenChars`. */

export const getDestination = () => {
  const copiedChars: string[] = [];

  return {

   /* The `writeChar` property is a function that is defined using `jest.fn()`. This creates a mock
   function that can be used to simulate the behavior of a `writeChar` function. */

    writeChar: jest.fn((c: string) => {
      copiedChars.push(c);
    }),

/* The `getWrittenChars` property is a function that returns the `copiedChars` array.
This function can be used to retrieve the characters that have been written to the destination. */

    getWrittenChars: () => copiedChars,

  };
};

Enter fullscreen mode Exit fullscreen mode

Now let's look at the code for our concrete Copy Class


\\Copy.ts

export class Copier {
  constructor(
    private readonly src: Source,
    private readonly dest: Destination
  ) {}

  /* The `copy()` method in the `Copier` class is responsible for copying characters from the source
  (`src`) to the destination (`dest`). */
  copy() {
    let char = this.src.readChar();

    while (char !== '\n') {
      this.dest.writeChar(char);

      char = this.src.readChar();
    }
  }
}

export interface Source {
  readChar(): string;
}

export interface Destination {
  writeChar(c: string): void;
}


Enter fullscreen mode Exit fullscreen mode

✅ Result : A passing test 🎉

A passing test


In a similar format , we impelement all the rest of the test cases too


//[+] Once character , ending in newline

  describe('when 1 character is read', () => {
    it.each([{ char: 'a' }, { char: 'b' }, { char: 'c' }, { char: 'd' }])(
      'reads first char from source',
      ({ char }) => {
        const src: Source = getSource([char]);

        const dest = getDestination();

        const sut = new Copier(src, dest);

        sut.copy();

        expect(dest.writeChar).toHaveBeenCalledWith(char);
      }
    );
  });

  //[+] Two characters , ending in newline
  describe('Two characters , ending in newline', () => {
    it.each([
      { chars: ['a', 'b'] },
      { chars: ['c', 'd'] },
      { chars: ['e', 'f'] },
    ])('reads exactly 2 times before encountering a newline', ({ chars }) => {
      const src: Source = getSource(chars);

      const dest = getDestination();

      const sut = new Copier(src, dest);

      sut.copy();

      expect(dest.writeChar).toHaveBeenCalledWith(chars[0]);
      expect(dest.writeChar).toHaveBeenCalledWith(chars[1]);
      expect(dest.writeChar).toBeCalledTimes(2);
    });
  });

  //[+] Many characters, ending in newline
  describe('Many characters, ending in newline', () => {
    it.each([
      { chars: ['a', 'b', 'c', 'd', 'g', 'c'] },
      { chars: ['c', 'd', 'e', 'f', 'd', 'f', 'd'] },
      { chars: ['e', 'f', 'g', 'd', 'd', ''] },
    ])(
      'reads exactly $chars.length times before encountering a newline',
      ({ chars }) => {
        const src: Source = getSource(chars);

        const dest = getDestination();

        const sut = new Copier(src, dest);

        sut.copy();

        expect(dest.writeChar).toBeCalledTimes(chars.length);

        //multiple characters written (order does not matter)
        chars.forEach((c) => expect(dest.writeChar).toHaveBeenCalledWith(c));

        // confirm last called character
        expect(dest.writeChar).toHaveBeenLastCalledWith(
          chars[chars.length - 1]
        );
      }
    );
  });

  //[+] Order of characters
  describe('Order of characters', () => {
    it.each([
      { chars: ['a', 'b', 'c', 'd', 'g', 'c'] },
      { chars: ['c', 'd', 'e', 'f', 'd', 'f', 'd'] },
      { chars: ['e', 'f', 'g', 'd', 'd', ''] },
    ])('has all characters copied in the same order', ({ chars }) => {
      const src: Source = getSource(chars);

      const dest = getDestination();

      const sut = new Copier(src, dest);

      sut.copy();

      expect(dest.writeChar).toBeCalledTimes(chars.length);

      expect(dest.getWrittenChars()).toEqual(chars);
    });
  });

  //[+] Characters after newline should not be written
  describe('Characters after newline should not be written', () => {
    it.each([
      {
        chars: [`1`, `2`, `3`, `4`, `5`, `6`, `7`, `\n`, `a`, `b`],
        before: [`1`, `2`, `3`, `4`, `5`, `6`, `7`],
        after: [`a`, `b`],
      },
      {
        chars: [`1`, `2`, `7`, `5`, `4`, `\n`, `c`, `d`],
        before: [`1`, `2`, `7`, `5`, `4`],
        after: [`c`, `d`],
      },
    ])(
      'has all characters before : $before , the newline  and none after : $after',
      ({ chars, before, after }) => {
        const src: Source = getSource(chars);

        const dest = getDestination();

        const sut = new Copier(src, dest);

        sut.copy();

        expect(dest.getWrittenChars()).toStrictEqual(before);
        expect(!isIntersection(after, dest.getWrittenChars())).toBe(true);
      }
    );
  });

Enter fullscreen mode Exit fullscreen mode

✅ Result : We pass all the tests 🔥🔥

Screenshot 2023-08-18 at 7.29.27 PM.png

We do need one more helper function isIntersection , that lets us find out if any element in one array is present in another array ( i.e Is there an intersection between the values of 2 arrays ? )


//copy.helper.ts

 /* The `isIntersection` function takes in two arrays `a` and `b` as parameters. It checks
if there is any element in array `a` that is also present in array `b`. It does this by
using the `some` method on array `a`, which returns `true` if at least one element
satisfies the provided condition. The condition being checked is whether `b` includes
the current element `e` from array `a`. If there is at least one element that satisfies
this condition, the function returns `true`, indicating that there is an intersection
between the two arrays. Otherwise, it returns `false`, indicating that there is no
intersection. */

export const isIntersection = (a: string[], b: string[]) =>a.some((e) => b.includes(e));

Enter fullscreen mode Exit fullscreen mode

👍 Great !! This concludes our tutorial .

If you want , you can have a look at the souce code on GitHub : Click Here To View Source Code

If you have any questions regarding the exercise , you're free to add a comment on the blog post and i'll try to answer it ASAP 🧑‍💻 !

Top comments (0)