DRY Testing — Do Not Repeat Yourself when Testing
TL;DR When testing do not repeat the code in the functions you are testing in the test itself.
I was coding up an integration test today which needed to check that values were being correctly set in Microsoft Word documents custom XML so that data bound controls would display properly.
I was testing five separate custom XML nodes and so I set up a data structure that held them:
const data = {
"/data/CHECKBOX": {
_type: "boolean",
value: true
},
"/data/DATE": {
_type: "date",
value: new Date("2019-04-01T12:00:00Z")
},
"/data/NUMBER": {
_type: "text",
value: "999"
},
"/data/RICH_TEXT": {
_type: "text",
value: `Example Rich Text
Empty line above`
},
"/data/TEXT": {
_type: "text",
value: `Example Text`
}
};
This happens to be the object that is used to set the values by the code.
Now to test it had been set. I wrote the document out to the filing system, then opened it and retrieved the internal custom XML document. So all I needed now was to test that the values were as expected.
I thought that in future if the values change I don’t want to have to change the test to match so I’ll just use the data object to do the testing.
document.childNodes
.filter(node => {
return Object.keys(data).includes(XmlNodePath.getPath(node));
})
.forEach(node => {
const key = XmlNodePath.getPath(node);
const property = (data as any)[key];
const textNode = XmlNode.lastTextChild(node);
expect(textNode.textContent).toBe(property.value);
});
I ran the test and it failed
expect(received).toBe(expected) // Object.is equalityExpected: 2019-04-01T12:00:00.000Z
Received: "2019-04-01"
This is actually expected behaviour as Word Data Bound controls expect dates to be in the format “yyyy-mm-dd”. I was about to write some code to automatically convert the expected if it was of type date when I realised that this was starting to repeat the code inside the functions I was testing. In fact the easy way to do this would be to just copy it, or maybe use it directly. Then I realised that by doing this I would not actually be testing that the code was working as expected as if there was an error in my logic it would use that self same logic in the test.
However if I went for a series of explicit tests then the arrange and assert would be separated and not obvious as the data was defined on line 20 but the test was on line 79.
expect(
XmlNode.lastTextChild(XmlNode.findChildByName(document, "DATE"))
.textContent
).toBe("2019-04-01");
So what I wanted was for the test data to be held with the expected results but for it to not use the code inside the functions. I added an expected value.
const data = {
"/data/CHECKBOX": {
_type: "boolean",
value: true,
expected: "true"
},
"/data/DATE": {
_type: "date",
value: new Date("2019-04-01T12:00:00Z"),
expected: "2019-04-01"
},
"/data/NUMBER": {
_type: "text",
value: "999",
expected: "999"
},
"/data/RICH_TEXT": {
_type: "text",
value: `Example Rich Text
Empty line above`,
expected: `Example Rich Text
Empty line above`
},
"/data/TEXT": {
_type: "text",
value: `Example Text`,
expected: `Example Text`
}
};
So now the values and their expected settings are next to each other.
The test itself can now become:
document.childNodes
.filter(node => {
return Object.keys(data).includes(XmlNodePath.getPath(node));
})
.forEach(node => {
const key = XmlNodePath.getPath(node);
const property = (data as any)[key];
const textNode = XmlNode.lastTextChild(node);
expect(textNode.textContent).toBe(property.expected);
});
So now I have a test which is easy to maintain and extend and doesn’t use a copy of the code inside it during the test.