Back to Repositories

Validating Family Financial Calculations in maybe-finance

This comprehensive test suite validates the Family model’s financial calculations and data synchronization capabilities in a Ruby on Rails application. It ensures accurate tracking of assets, liabilities, net worth, and transaction analytics while maintaining data integrity across Plaid integrations and manual accounts.

Test Coverage Overview

The test suite provides extensive coverage of core financial calculations and data management:

  • Financial calculations for assets, liabilities, and net worth
  • Account synchronization with Plaid integration
  • Transaction analysis and rolling totals
  • Account status management (active/disabled)
  • Historical balance snapshots and time series data

Implementation Analysis

The testing approach employs Minitest’s assertion-based methodology with modular test organization. It leverages mock objects for external service integration and includes helper modules for common testing patterns. The implementation focuses on isolated unit tests with clear setup and teardown procedures.

Key patterns include fixture usage, helper method abstraction, and comprehensive edge case coverage.

Technical Details

Testing tools and configuration:

  • Minitest framework with ActiveSupport::TestCase
  • Custom test helpers for account entries and syncable interfaces
  • Money gem for currency handling
  • CSV module for data processing
  • Mocha for method stubbing and expectations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear setup
  • Comprehensive edge case coverage
  • Proper mock object usage
  • DRY principle through helper methods
  • Clear test naming conventions
  • Consistent assertion patterns

maybe-finance/maybe

test/models/family_test.rb

            
require "test_helper"
require "csv"

class FamilyTest < ActiveSupport::TestCase
  include Account::EntriesTestHelper
  include SyncableInterfaceTest

  def setup
    @family = families(:empty)
    @syncable = families(:dylan_family)
  end

  test "syncs plaid items and manual accounts" do
    family_sync = syncs(:family)

    manual_accounts_count = @syncable.accounts.manual.count
    items_count = @syncable.plaid_items.count

    Account.any_instance.expects(:sync_data)
      .with(start_date: nil)
      .times(manual_accounts_count)

    PlaidItem.any_instance.expects(:sync_data)
      .with(start_date: nil)
      .times(items_count)

    @syncable.sync_data(start_date: family_sync.start_date)
  end

  test "calculates assets" do
    assert_equal Money.new(0, @family.currency), @family.assets

    create_account(balance: 1000, accountable: Depository.new)
    create_account(balance: 5000, accountable: OtherAsset.new)
    create_account(balance: 10000, accountable: CreditCard.new) # ignored

    assert_equal Money.new(1000 + 5000, @family.currency), @family.assets
  end

  test "calculates liabilities" do
    assert_equal Money.new(0, @family.currency), @family.liabilities

    create_account(balance: 1000, accountable: CreditCard.new)
    create_account(balance: 5000, accountable: OtherLiability.new)
    create_account(balance: 10000, accountable: Depository.new) # ignored

    assert_equal Money.new(1000 + 5000, @family.currency), @family.liabilities
  end

  test "calculates net worth" do
    assert_equal Money.new(0, @family.currency), @family.net_worth

    create_account(balance: 1000, accountable: CreditCard.new)
    create_account(balance: 50000, accountable: Depository.new)

    assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth
  end

  test "should exclude disabled accounts from calculations" do
    cc = create_account(balance: 1000, accountable: CreditCard.new)
    create_account(balance: 50000, accountable: Depository.new)

    assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth

    cc.update! is_active: false

    assert_equal Money.new(50000, @family.currency), @family.net_worth
  end

  test "calculates snapshot" do
    asset = create_account(balance: 500, accountable: Depository.new)
    liability = create_account(balance: 100, accountable: CreditCard.new)

    asset.balances.create! date: 1.day.ago.to_date, currency: "USD", balance: 450
    asset.balances.create! date: Date.current, currency: "USD", balance: 500

    liability.balances.create! date: 1.day.ago.to_date, currency: "USD", balance: 50
    liability.balances.create! date: Date.current, currency: "USD", balance: 100

    expected_asset_series = [
      { date: 1.day.ago.to_date, value: Money.new(450) },
      { date: Date.current, value: Money.new(500) }
    ]

    expected_liability_series = [
      { date: 1.day.ago.to_date, value: Money.new(50) },
      { date: Date.current, value: Money.new(100) }
    ]

    expected_net_worth_series = [
      { date: 1.day.ago.to_date, value: Money.new(450 - 50) },
      { date: Date.current, value: Money.new(500 - 100) }
    ]

    assert_equal expected_asset_series, @family.snapshot[:asset_series].values.map { |v| { date: v.date, value: v.value } }
    assert_equal expected_liability_series, @family.snapshot[:liability_series].values.map { |v| { date: v.date, value: v.value } }
    assert_equal expected_net_worth_series, @family.snapshot[:net_worth_series].values.map { |v| { date: v.date, value: v.value } }
  end

  test "calculates top movers" do
    checking_account = create_account(balance: 500, accountable: Depository.new)
    savings_account = create_account(balance: 1000, accountable: Depository.new)
    create_transaction(account: checking_account, date: 2.days.ago.to_date, amount: -1000)
    create_transaction(account: checking_account, date: 1.day.ago.to_date, amount: 10)
    create_transaction(account: savings_account, date: 2.days.ago.to_date, amount: -5000)

    zero_income_zero_expense_account = create_account(balance: 200, accountable: Depository.new)
    create_transaction(account: zero_income_zero_expense_account, amount: 0)

    snapshot = @family.snapshot_account_transactions
    top_spenders = snapshot[:top_spenders]
    top_earners = snapshot[:top_earners]
    top_savers = snapshot[:top_savers]

    assert_equal [ 10 ], top_spenders.map(&:spending)
    assert_equal [ 5000, 1000 ], top_earners.map(&:income)
    assert_equal [ 1, 0.99 ], top_savers.map(&:savings_rate)
  end


  test "calculates rolling transaction totals" do
    account = create_account(balance: 1000, accountable: Depository.new)
    liability_account = create_account(balance: 1000, accountable: Loan.new)
    create_transaction(account: account, date: 2.days.ago.to_date, amount: -500)
    create_transaction(account: account, date: 1.day.ago.to_date, amount: 100)
    create_transaction(account: account, date: Date.current, amount: 20)
    create_transaction(account: liability_account, date: 2.days.ago.to_date, amount: -333)

    snapshot = @family.snapshot_transactions

    expected_income_series = [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 500, 500, 500
    ]

    assert_equal expected_income_series, snapshot[:income_series].values.map(&:value).map(&:amount)

    expected_spending_series = [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 100, 120
    ]

    assert_equal expected_spending_series, snapshot[:spending_series].values.map(&:value).map(&:amount)

    expected_savings_rate_series = [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 1, 0.8, 0.76
    ]

    assert_equal expected_savings_rate_series, snapshot[:savings_rate_series].values.map(&:value).map { |v| v.round(2) }
  end

  private

    def create_account(attributes = {})
      account = @family.accounts.create! name: "Test", currency: "USD", **attributes
      account
    end
end