Back to Repositories

Validating Security Price Operations in maybe-finance

This test suite validates the Security::Price model’s functionality for managing and retrieving security price data in the Maybe Finance application. It covers both database operations and external API interactions for fetching security prices.

Test Coverage Overview

The test suite provides comprehensive coverage of security price operations:

  • Single and multiple price retrieval from database
  • Price caching mechanisms
  • External API integration for missing prices
  • Error handling for missing API keys
  • Date range price fetching

Implementation Analysis

The testing approach utilizes Minitest’s mocking capabilities to isolate the price provider integration. It implements stubbing patterns for external dependencies and environment variables, while validating both successful and failure scenarios for price retrieval operations.

The tests demonstrate proper use of ActiveSupport::TestCase features and mock objects for controlled testing of external service interactions.

Technical Details

Key technical components include:

  • Minitest framework with ActiveSupport::TestCase
  • Mock objects for API integration testing
  • Environment variable management utilities
  • Database fixtures for test data
  • OpenStruct for response simulation

Best Practices Demonstrated

The test suite exhibits several testing best practices:

  • Proper test isolation using mocks and stubs
  • Comprehensive edge case coverage
  • Clear test naming conventions
  • Efficient use of test fixtures
  • Thorough validation of both success and failure paths

maybe-finance/maybe

test/models/security/price_test.rb

            
require "test_helper"
require "ostruct"

class Security::PriceTest < ActiveSupport::TestCase
  setup do
    @provider = mock

    Security::Price.stubs(:security_prices_provider).returns(@provider)
  end

  test "security price provider nil if no api key provided" do
    Security::Price.unstub(:security_prices_provider)

    with_env_overrides SYNTH_API_KEY: nil do
      assert_not Security::Price.security_prices_provider
    end
  end

  test "finds single security price in DB" do
    @provider.expects(:fetch_security_prices).never
    security = securities(:aapl)

    price = security_prices(:one)

    assert_equal price, Security::Price.find_price(security: security, date: price.date)
  end

  test "caches prices to DB" do
    expected_price = 314.34
    security = securities(:aapl)
    tomorrow = Date.current + 1.day

    @provider.expects(:fetch_security_prices)
            .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: tomorrow, end_date: tomorrow)
            .once
            .returns(
              OpenStruct.new(
                success?: true,
                prices: [ { date: tomorrow, price: expected_price, currency: "USD" } ]
              )
            )

    fetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)
    refetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)

    assert_equal expected_price, fetched_rate.price
    assert_equal expected_price, refetched_rate.price
  end

  test "returns nil if no price found in DB or from provider" do
    security = securities(:aapl)
    Security::Price.delete_all # Clear any existing prices

    @provider.expects(:fetch_security_prices)
             .with(ticker: security.ticker, mic_code: security.exchange_mic, start_date: Date.current, end_date: Date.current)
             .once
             .returns(OpenStruct.new(success?: false))

    assert_not Security::Price.find_price(security: security, date: Date.current)
  end

  test "returns nil if price not found in DB and provider disabled" do
    Security::Price.unstub(:security_prices_provider)
    security = Security.new(ticker: "NVDA")

    with_env_overrides SYNTH_API_KEY: nil do
      assert_not Security::Price.find_price(security: security, date: Date.current)
    end
  end

  test "fetches multiple dates at once" do
    @provider.expects(:fetch_security_prices).never
    security = securities(:aapl)
    price1 = security_prices(:one) # AAPL today
    price2 = security_prices(:two) # AAPL yesterday

    fetched_prices = Security::Price.find_prices(security: security, start_date: 1.day.ago.to_date, end_date: Date.current).sort_by(&:date)

    assert_equal price1, fetched_prices[1]
    assert_equal price2, fetched_prices[0]
  end

  test "caches multiple prices to DB" do
    missing_price = 213.21
    security = securities(:aapl)

    @provider.expects(:fetch_security_prices)
             .with(ticker: security.ticker,
                  mic_code: security.exchange_mic,
                  start_date: 2.days.ago.to_date,
                  end_date: 2.days.ago.to_date)
             .returns(OpenStruct.new(success?: true, prices: [ { date: 2.days.ago.to_date, price: missing_price, currency: "USD" } ]))
             .once

    price1 = security_prices(:one) # AAPL today
    price2 = security_prices(:two) # AAPL yesterday

    fetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)
    refetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)

    assert_equal [ missing_price, price2.price, price1.price ], fetched_prices.sort_by(&:date).map(&:price)
    assert_equal [ missing_price, price2.price, price1.price ], refetched_prices.sort_by(&:date).map(&:price)

    assert Security::Price.exists?(security: security, date: 2.days.ago.to_date, price: missing_price)
  end

  test "returns empty array if no prices found in DB or from provider" do
    Security::Price.unstub(:security_prices_provider)

    with_env_overrides SYNTH_API_KEY: nil do
      assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current)
    end
  end
end