A great kata for introducing jest mocks functions in your project.
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 theJest
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);
});
});
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,
};
};
//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,
};
};
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;
}
✅ Result : 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);
}
);
});
✅ Result : We pass all the tests 🔥🔥
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));
👍 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)