DEV Community

Cover image for Writing User Info to files in CSV format ( TDD Kata ) - Part 1
Chiranjeev Thomas
Chiranjeev Thomas

Posted on

Writing User Info to files in CSV format ( TDD Kata ) - Part 1

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 :

Screenshot 2023-08-24 at 5.08.47 PM.png




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;

Enter fullscreen mode Exit fullscreen mode

\\FileWriter.ts

interface FileWriter {
  writeLine(fileName: string, line: string): void;
}

export default FileWriter;

Enter fullscreen mode Exit fullscreen mode

//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}`;
  };
}

Enter fullscreen mode Exit fullscreen mode

( 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

  1. GIVEN ( the starting condition )
  2. WHEN ( some action occurs )
  3. 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);

  });
Enter fullscreen mode Exit fullscreen mode

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);
};

Enter fullscreen mode Exit fullscreen mode

Result : ✅ Passing Test

Screenshot 2023-08-24 at 6.14.32 PM.png


🧐 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)
    );
  });
Enter fullscreen mode Exit fullscreen mode

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}`;
};

Enter fullscreen mode Exit fullscreen mode

❌ 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}`;
  };
}

Enter fullscreen mode Exit fullscreen mode
\\ 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}`;
  };
}
Enter fullscreen mode Exit fullscreen mode

Result : ✅ Passing test !
Screenshot 2023-08-24 at 6.40.37 PM.png


🧐 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
        }`
      );
    }


Enter fullscreen mode Exit fullscreen mode

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;
};


Enter fullscreen mode Exit fullscreen mode

❌ 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));

    });

  }  }

Enter fullscreen mode Exit fullscreen mode

Result : ✅ Our tests for multiple customer pass 🔥🔥

Screenshot 2023-08-24 at 7.42.16 PM.png






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 .


Click Here : GitHub Code Repo


Top comments (0)