Building a Multi-currency Portfolio with Javascript and Test Driven Development

Introduction

In this article, we will be solving the money problem, not the general one we all have which is its insufficiency, but rather managing a portfolio of multiple currencies. By the end of this article, you should be able to

  • Understand the principle of Test Driven Development

  • Understand and apply the Object-oriented feature of Javascript

  • Apply test-driven development using Jest

  • Under the principle of Separation of Concern (SoC) in Software Development

Problem Definition and Requirements Highlight

In software development, the quality of our code is measured by how much value it provides, and the ease at which these values can be enhanced. There's a need for clarity in the problem we are solving to provide value.

For the system we are creating, we have the following requirements

  • Users should be able to create different currencies

  • Currency operations should be allowed (Addition, Subtraction, and Multiplication of the same currency)

  • The user should be able to do multi-currency operations i.e 1 USD + 1 EUR = 2.2 USD

  • The user should be able to see the total worth of the portfolio in a defined currency. ( 10 USD + 10 EUR + 100 JPY = 72 USD)

**Approaching The Problem

**

We could solve this problem using a functional programming approach or Object Oriented Programming (OOP) approach. But we notice some recurring terms in the requirement. MONEY, CURRENCY, and OPERATIONS. That being said, we need to create an entity Money, which we can do some operations on it. Also, we need to tick the boxes one after the other and ensure we can extend our portfolio features later, this brings about the need for Test-driven Development That out of the way, we need to set up our project.

To follow along with this, we will need to have the following on our development environment.

  • Nodejs

  • A suitable IDE

We create a directory called, portfolio-tdd and then Initialize a Node.js project

mkdir portfolio-tdd cd portfolio-tdd
npm init -y

As we noted earlier, we need a test runner and also make some assertion to ensure our application work as it is meant to. This is where Jest comes into the picture. We will install jest with the following command
npm install -D jest

Now our project is set for the journey into the Money land.

Test-Driven Development Primer TDD is simply Red - > Green - > Refactor.
Red = We write a failing test for the smallest part of the feature we are building
Green = We write the minimal code to ensure the test passes
Refactor = We improve the code we wrote in the green section with best practices (removing hard-coded values and magic numbers).

  • Checklist 1: Creating various currency

As we agreed to use Test Driven approach, we need to start with a failing test (Red Phase). We will create a file portfolio.test.js, this is the file that will contain all our tests for this project.
In the file, we will add the following piece of code.

describe("Porfolio", () => {
    test("It should create  currency", () => {
        const tenDollar = new Money(10, "USD");
        const tenEuro = new Money(10, "EUR");
        expect(tenDollar.amount).toBe(10);
        expect(tenEuro.amount).toBe(10);
        expect(tenDollar.currency).toBe("USD");
        expect(tenEuro.currency).toBe("EUR");
    });
})

The piece of code above is just the javascript way of saying

'Heyyo, get me 10 Dollars and 10 Euro, and check if have the correct currency and value '

In our IDE, we open a terminal and run the command

jest

We get an output like the one shown in the image below

We get an error, so our red phase is on. Now we move to the green phase.

We will create a new file money.js, where we will define the money class. in the money.js file, we will add the following code.

class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }
}

module.exports = Money;

What we did here is basically saying, Hey create a template for me, it will have an amount and a currency. then make sure every other person has access to this template.

Now if we run jest again, it should still fail. Why? you ask, Well, we need to ensure the test file has access to the Money Class we just created.

At the top of the portfolio.test.js file, we add this line of code.

// portfolio.test.js
const Money = require("./money");

describe("Portfolio", ()=>{
  // other parts remain the same
})

Now if we run jest again, our test passes, and we get an output like this โฌโฌ

  • Checklist 2: Same currency operation (Add, Multiply, and Division operation)

    Now we are able to literally create Money, it will be nice to have some basic operation with it. We go for the red phase again, we write a test for basic operation as shown below

const Money = require("./money");

describe("Porfolio", () => {
    test("It should create  currency", () => {
        const tenDollar = new Money(10, "USD");
        const tenEuro = new Money(10, "EUR");
        expect(tenDollar.amount).toBe(10);
        expect(tenEuro.amount).toBe(10);
        expect(tenDollar.currency).toBe("USD");
        expect(tenEuro.currency).toBe("EUR");
    });

    test("It should add same currency", () => {
        const tenDollar = new Money(10, "USD");
        const twentyDollar = new Money(20, "USD");
        const thirtyDollar = tenDollar.add(twentyDollar);
        expect(thirtyDollar.amount).toBe(30);
        expect(thirtyDollar.currency).toBe("USD");
    });
    test("it should not subtract the same currency", () => {
        const tenDollar = new Money(10, "USD");
        const twentyDollar = new Money(20, "USD");
        const minusTenDollar = twentyDollar.subtract(tenDollar);
        expect(minusTenDollar.amount).toBe(10);
        expect(minusTenDollar.currency).toBe("USD");
    });
    test("it should multiply the same currency", () => {
        const tenDollar = new Money(10, "USD");
        const hundredDollar = tenDollar.multiply(10);
        expect(hundredDollar.amount).toBe(100);
        expect(hundredDollar.currency).toBe("USD");
    });
    test("it should divide the same currency", () => {
        const tenDollar = new Money(10, "USD");
        const oneDollar = tenDollar.divide(10);
        expect(oneDollar.amount).toBe(1);
        expect(oneDollar.currency).toBe("USD");
    })
})

Now all these tests should fail. We go back to our money.js file to add some functionality to it.

We update the Money class to look like the code shown below.

class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

// addition of money
add(money) {
        return new Money(this.amount + money.amount, this.currency)
    }

// subtraction of money
subtract(money) {
        return new Money(this.amount - money.amount, this.currency);
    }
// multiplying money by a value (multiplier)
    multiply(multiplier) {
        return new Money(this.amount * multiplier, this.currency);
    }
// reducing money by a value (divisor)
    divide(divisor) {
        return new Money(this.amount / divisor, this.currency);
    }

}

module.exports = Money;

If we run jest, all our test pass. Hurray ๐Ÿ’ž๐Ÿ’ž, We are a step closer to creating our own bank.

  • Checklist 3: Multicurrency operation

The beauty of having money is to use it in other places, so we need to include a feature to ensure we can work with other currencies.

We could do a lot of operations with two different currencies, but we will focus on just the addition of different currencies for now. But remember, in our Money class, we have an add method already, we just need to update it and also get the conversion rate of one currency to the other. So we add one more test to our test suite as shown below

//portfolio.test.js 


test("it should add two different currency", () => {
        const tenDollar = new Money(10, "USD");
        const tenEuro = new Money(10, "EUR");
        const expectedValue = tenEuro.add(tenDollar);
        expect(expectedValue.amount).toBe(22);
        expect(expectedValue.currency).toBe("EUR");
    });

So our add method will now look like this. Basically, what this means is, if the two currency types are the same, just add their value together, else, multiply the amount of one with the conversion rate, then add the result to the amount of the second currency. Here we hard code the conversion rate of EUR -> USD

class Money {

// other part remain the same
add(money) {
        let EURUSD = 1.2;
        if (this.currency === money.currency) {
            return new Money(this.amount + money.amount, this.currency);
        }
        return new Money(this.amount * EURUSD + money.amount, this.currency)
    }

// ... remains the same
}

Now, if we run jest, all the tests pass again.

  • Checklist 4: Sum a list of currencies and get the value in another currency

    But what if we want to add more than two currencies together?๐Ÿค”

    Should we also add this logic to our Money class?

    This is where we need to rethink our approach to see where we need to refactor. But when we have more than one currency, that is like a wallet or a portfolio. Yeah, we got our new Entity, A portfolio, this entity should handle the conversion and addition of multiple currencies together.

    This process of separating the functionality of our entity is known as the principle of Separation of Concerns (SoC).

    Now we need to create a new file portfolio.js, the content should be as shown below

      // portfolio.js
    
      const Money = require("./money")
    
      class Portfolio {
          constructor() {
              this.exchangeRate = new Map();
          }
      }
      module.exports = Portfolio;
    

    N.B: We created a map for our exchange rate, this is a key-value store, where the key is the currencies and the value is the conversion rate

    Now we write a failing test the conversion rate functionality

      test("it should convert to different currency", () => {
              const twelveDollar = new Money(12, "USD");
              const tenEuro = new Money(10, "EUR");
              const portfolio = new Portfolio();
              const expectedValue = portfolio.convert(tenEuro, "USD");
              expect(expectedValue).toMatchObject(twelveDollar);
          })
    

    Now we need a convert method in our portfolio to pass this test. So we update our portfolio class to look like the one shown below

      convert(money, currency) {
              this.exchangeRate.set("EUR->USD", 1.2);
              if (money.currency === currency) {
                  return new Money(money.amount, currency);
              }
              let key = money.currency+ "->" + currency;
              if (this.exchangeRate.has(key)) {
                  let rate = this.exchangeRate.get(key);
                  return new Money(money.amount * rate, currency);
              }
              throw new Error("No exchange rate found for " + key);
          }
    

    Basically, we hardcoded the conversion rate of EUR => USD, and then check the conversion rate, which we will use to multiply the amount. and if we don't find the key for that currency, we throw an error.

    Now our test pass again. Now, we are almost done. But wait. What if we want to add more than two different currencies together?

    Let's write a test for that to see if our code catch that also.

          test("it should add various currencies in portfolio to USD", () => {
              const tenDollar = new Money(10, "USD");
              const tenEuro = new Money(10, "EUR");
              let hundredYen = new Money(100, "JPY");
              const portfolio = new Portfolio();
              const expectedValue = portfolio.addMoney([tenDollar, hundredYen, tenEuro,], "USD");
              expect(expectedValue.amount).toBe(72);
              expect(expectedValue.currency).toBe("USD");
          });
    

    Oops, we did not cover this loophole, now let's fix it in the portfolio class. In the portfolio class, we will add a new method for adding a list of currencies as shown below.

      // portfolio.js
    
      addMoney(moneyArray, currency) {
              let totalAmount = moneyArray.reduce((acc, money) => {
                  let convertedMoney = this.convert(money, currency);
                  console.log(convertedMoney);
                  return acc + convertedMoney.amount;
              }, 0);
              return new Money(totalAmount, currency);
          }
    

    Our test still fails, because we don't have a conversion rate for JPY -> USD, Now we assume 1 USD = 2 JPY, then conversely, 1 JPY = 0.5 USD,

We will hardcode this value in our map of exchange rates.

So we will update the convert method to

     convert(money, currency) {
            this.exchangeRate.set("EUR->USD", 1.2);
            this.exchangeRate.set("JPY->USD", 0.5);
            // .... other lines remain the same
    }

Now if we run jest , our tests are all green as shown below.

Conclusion

In this article, we created a portfolio of currency, but there is a caveat here, we focused on the upside of our application alone, and we did not account for some issues and downside that arises with money in general, also we hardcoded some values.

In general, we learned how to develop features in a piece-wise manner and also ensure we don't break any part of our code while updating our system.

Aside from writing articles, I drop some useful software development tips and nuggets on my social media also, You can check out some of them on Linkedin or Twitter.

0
Subscribe to my newsletter

Read articles from Abdulsalam Lukmon directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abdulsalam Lukmon
Abdulsalam Lukmon