How does one handle changes to the requirements in a real-life setting while building software using Test Driven Development?
This article explores different techniques one can apply in situations where requirements for a feature keep changing constantly
Summary :
How does one handle changes in feature requirements while building software using Test Driven Development ?
This article explores different techniques one can apply in situations where requirements for a feature keep changing constantly .
We will require the knowledge of S.O.L.I.D principles , Typescript and some knowledge of creating and using mocks in Jest ( However , you're free to use whichever language and testing framework you like )
There will be 4 different parts to this exercise , reflecting the different requirements that were brought in by the product manager at different stages of the prouduct development process
Task 1 : Write Customer information to a file in CSV format
Task 2 : Write Customer information in batches of N customers at a time
Task 3 : Wrire Customer information , with no duplicate entries
Task 4 : Write Customer information to 2 different sources ( one with duplicates and one without for monitoring purposes )
In this specific post , we'll be concerntating on the 1st task and getting things set up !
First Let's look at the UML Diagram for the particular situation :
Code :
//Customer.ts
class Customer {
constructor(private _name: string, private _contactNumber: string) {}
constructor(private _name: string, private _contactNumber: string) {}
public get name(): string {
return this._name;
}
public set name(value: string) {
this._name = value;
}
public get contactNumber(): string {
return this._contactNumber;
}
public set contactNumber(value: string) {
this._contactNumber = value;
}
}
export default Customer;
\\FileWriter.ts
interface FileWriter {
writeLine(fileName: string, line: string): void;
}
export default FileWriter;
//CustomerFileWriter.ts
export class CustomerFileWriter {
constructor(private readonly fileWriter: FileWriter) {}
writeCustomers(fileName: string, customers: Customer[]) {
// to implement !!!
}
private customerToString = (customer: Customer) => {
return `${customer.name},${customer.contactNumber}`;
};
}
( We'll be exploring a slightly different flavor of Test Driven Development called Behavior Driven Development )
What separates this methodology from traditional T.D.D is the focus on getting a feature to work, and single-mindedly writing tests to achieve the same.
Personally, I aim to get the happy paths for a given feature passing, as soon as possible !
The are 3 things you should always keep in mind while formulating a B.D.D test
GIVEN ( the starting condition )
WHEN ( some action occurs )
THEN ( expected result achieved )
To further discuss the test cases for the behavior we expect when attempting to write a customer's information to a file, we can consider the following scenarios:
Case 1 :
given : there is no customer object in the customers array
when : we try to write customers to a file
then : nothing is written to a file ( i.e writeLine function is not called)
\\customer-file-writer.test.ts
it(' will not write to file , when there is no customer', () => {
//arrange
const fileWriter = getFileWriter();
const fileName = 'myfile.pdf';
//act
const sut = getCustomerFileWriter(fileWriter);
sut.writeCustomers(fileName, []);
//assert
expect(fileWriter.writeLine).toBeCalledTimes(0);
});
Helper functions :
\\customer-file-writer.helper.ts
export const getFileWriter = (): FileWriter => {
return {
writeLine: jest.fn((_fileName: string, _line: string) => {}),
};
};
export const getCustomerFileWriter = (
fileWriter: FileWriter
): ICustomerFileWriter => {
return new CustomerFileWriter(fileWriter);
};
Result : ✅ Passing Test
🧐 Let's reflect on what we just did :
We are able to pass this test without even implementing any logic ! Great !So now what would happen when we have 1 customer is the customer array ?
What do we expect in that case ?
Case 2 :
given : single customer object is present
when : we try to write the object info to file
then : only single object with the given info gets written
\\customer-file-writer.test.ts
it('single customer object is present', () => {
//arrange
const fileWriter = getFileWriter();
const customer = getCustomer('Glen', '32');
const fileName = 'myfile.pdf';
//act
const sut = getCustomerFileWriter(fileWriter);
sut.writeCustomers(fileName, [customer]);
//assert
expect(fileWriter.writeLine).toBeCalledTimes(1);
expect(fileWriter.writeLine).toHaveBeenLastCalledWith(
fileName,
customerToString(customer)
);
});
Helper function :
//customer-file-writer.helper.ts
export const getCustomer = (name: string, contactNumber: string): Customer => {
return new Customer(name, contactNumber);
};
export const customerToString = (customer: Customer) => {
return `${customer.name},${customer.contactNumber}`;
};
❌ Now if you try to run the test , you'll encounter a test failure .
To mitigate that you'll have to write the least possible code in CustomerFileWriter to get the test passing
Before Modification : customer-file-writer.ts
//customer-file-writer.ts
export class CustomerFileWriter {
constructor(private readonly fileWriter: FileWriter) {}
writeCustomers(fileName: string, customers: Customer[]) {
// to implement !!!
}
private customerToString = (customer: Customer) => {
return `${customer.name},${customer.contactNumber}`;
};
}
\\ Modified : customer-file-writer.ts
export class CustomerFileWriter implements ICustomerFileWriter {
constructor(private readonly fileWriter: FileWriter) {}
writeCustomers(fileName: string, customers: Customer[]) {
// this is the single implementation change we make
this.fileWriter.writeLine(fileName, this.customerToString(customers[0]));
//
}
private customerToString = (customer: Customer) => {
return `${customer.name},${customer.contactNumber}`;
};
}
🧐 Let's reflect on what we did :
We changed a single line , and wrote a single customer's information to a file .
Will this work for multiple customers ?
Case 3 :
given : multiple customer objects are present
when : we try to write the objects info to file
then : the exact number and the exact order of customers are written to file
\\ customer-file-writer.test.ts
it.each([
{
customers: createCustomers(3),
},
{
customers: createCustomers(12),
},
])(
'$customers.length customers info gets written , in the proper order ',
({ customers }) => {
//arrange
const fileWriter = getFileWriter();
const fileName = 'narnia.ts';
//act
const sut = getCustomerFileWriter(fileWriter);
sut.writeCustomers(fileName, customers);
//assert
assertCustomersHaveBeenWritten(fileWriter, fileName, customers);
//has same number of items as the number of customers
expect(fileWriter.writeLine).toHaveBeenCalledTimes(customers.length);
// items written in same order , i.e last item written last
expect(fileWriter.writeLine).toHaveBeenLastCalledWith(
fileName,
`${customers[customers.length - 1].name},${
customers[customers.length - 1].contactNumber
}`
);
}
Helper Function :
//customer-file-writer.helper.ts
export const createCustomers = (count: number) => {
const customers: Customer[] = [];
for (let i = 1; i <= count; i++) {
customers.push(getCustomer(i + '', i + ''));
}
return customers;
};
❌ This again results in a test failure
Now, let's again think about the least amount of code we can write to make the tests pass .
\\ Modified : customer-file-writer.ts
export class CustomerFileWriter implements ICustomerFileWriter {
constructor(private readonly fileWriter: FileWriter) {}
writeCustomers(fileName: string, customers: Customer[]) {
// we modified this line : this.fileWriter.writeLine(fileName, this.customerToString(customers[0]));
// rather than writing a single customer , this time , we write multiple customers
customers.forEach((customer) => {
this.fileWriter.writeLine(fileName, this.customerToString(customer));
});
} }
Result : ✅ Our tests for multiple customer pass 🔥🔥
That's all for now !
Stay tuned for when our product manager arrives with a new set of modifications for our Customer File Writer feature .
We'll have to modify and adapt to the new requrements , and ensure we do the least amount of modification to our code and keep our implementation SOLID compliant .
Top comments (0)