Write the bank core package

Completed

Now that we have the base project running along with our test file, let's start writing the code that implements the previous unit's features and requirements. Here, we revisit a few subjects we discussed earlier, like errors, structures, and methods.

Open the $GOPATH/src/bankcore/bank.go file, remove the Hello() function, and let's start writing the core logic of our online bank system.

Create structures for customers and accounts

Let's begin by creating a Customer structure where we have the name, address, and phone number from a person who wants to become a bank customer. Also, we need a structure for the Account data. Because a customer can have more than one account, let's embed the customer information into the account object. Basically, let's create what we defined in the TestAccount test.

The structures that we need might look like the following code example:

package bank

// Customer ...
type Customer struct {
    Name    string
    Address string
    Phone   string
}

// Account ...
type Account struct {
    Customer
    Number  int32
    Balance float64
}

When you run the go test -v command in your terminal now, you should see that the test is passing:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
PASS
ok      github.com/msft/bank    0.094s

This test is passing because we implemented the structures for Customer and Account. Now that we have the structures, let's write the methods for adding the features that we need in the initial version of our bank. These features include deposit, withdraw, and transfer money.

Implement the deposit method

We need to start with a method to allow adding money to our account. But before we do that, let's create the TestDeposit function in the bank_test.go file:

func TestDeposit(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(10)

    if account.Balance != 10 {
        t.Error("balance is not being updated after a deposit")
    }
}

When you run go test -v, you should see a failing test in the output:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:32:9: account.Deposit undefined (type Account has no field or method Deposit)
FAIL    github.com/msft/bank [build failed]

To satisfy the previous test, let's create a Deposit method to our Account structure that returns an error if the amount received is equal to or lower than zero. Otherwise, just add the amount received to the balance of the account.

Use the following code for the Deposit method:

// Deposit ...
func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("the amount to deposit should be greater than zero")
    }

    a.Balance += amount
    return nil
}

When you run go test -v, you should see that the test is passing:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
PASS
ok      github.com/msft/bank    0.193s

You can also write a test that confirms that you get an error when you try to deposit a negative amount, like this:

func TestDepositInvalid(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    if err := account.Deposit(-10); err == nil {
        t.Error("only positive numbers should be allowed to deposit")
    }
}

When you run the go test -v command, you should see that the test is passing:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
PASS
ok      github.com/msft/bank    0.197s

Note

From here on, we'll write one test case for each method. But you should write as many tests to your programs as you feel comfortable with, so you can cover both expected and unexpected scenarios. For example, in this case, the error-handling logic is tested.

Implement the withdraw method

Before we write the Withdraw functionality, let's write the test for it:

func TestWithdraw(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(10)
    account.Withdraw(10)

    if account.Balance != 0 {
        t.Error("balance is not being updated after withdraw")
    }
}

When you run the go test -v command, you should see a failing test in the output:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:67:9: account.Withdraw undefined (type Account has no field or method Withdraw)
FAIL    github.com/msft/bank [build failed]

Let's implement the logic for the Withdraw method, where we reduce the balance of the account by the amount that we receive as a parameter. Like we did before, we need to validate that the number we receive is greater than zero and that the balance in the account is enough.

Use the following code for the Withdraw method:

// Withdraw ...
func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("the amount to withdraw should be greater than zero")
    }

    if a.Balance < amount {
        return errors.New("the amount to withdraw should be less than the account's balance")
    }

    a.Balance -= amount
    return nil
}

When you run the go test -v command, you should see that the test is passing:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
=== RUN   TestWithdraw
--- PASS: TestWithdraw (0.00s)
PASS
ok      github.com/msft/bank    0.250s

Implement the statement method

Let's write a method to print the statement that includes the account name, number, and balance. But first, let's create the TestStatement function:

func TestStatement(t *testing.T) {
    account := Account{
        Customer: Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number:  1001,
        Balance: 0,
    }

    account.Deposit(100)
    statement := account.Statement()
    if statement != "1001 - John - 100" {
        t.Error("statement doesn't have the proper format")
    }
}

When you run go test -v, you should see a failing test in the output:

# github.com/msft/bank [github.com/msft/bank.test]
./bank_test.go:86:22: account.Statement undefined (type Account has no field or method Statement)
FAIL    github.com/msft/bank [build failed]

Let's write the Statement method, which should return a string. (You have to overwrite this method later as a challenge.) Use the following code:

// Statement ...
func (a *Account) Statement() string {
    return fmt.Sprintf("%v - %v - %v", a.Number, a.Name, a.Balance)
}

When you run go test -v, you should see that the test is passing:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
=== RUN   TestDeposit
--- PASS: TestDeposit (0.00s)
=== RUN   TestDepositInvalid
--- PASS: TestDepositInvalid (0.00s)
=== RUN   TestWithdraw
--- PASS: TestWithdraw (0.00s)
=== RUN   TestStatement
--- PASS: TestStatement (0.00s)
PASS
ok      github.com/msft/bank    0.328s

Let's move on to the next section and write the Web API that exposes the Statement method.