mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
88 lines
3.3 KiB
Text
88 lines
3.3 KiB
Text
|
---
|
||
|
description:
|
||
|
globs: test/**
|
||
|
alwaysApply: false
|
||
|
---
|
||
|
Use this rule to learn how to write tests for the Maybe codebase.
|
||
|
|
||
|
Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.
|
||
|
|
||
|
- **General testing rules**
|
||
|
- Always use Minitest and fixtures for testing, NEVER rspec or factories
|
||
|
- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed.
|
||
|
- For tests that require a large number of fixture records to be created, use Rails helpers to help create the records needed for the test, then inline the creation. For example, [entries_test_helper.rb](mdc:test/support/entries_test_helper.rb) provides helpers to easily do this.
|
||
|
|
||
|
- **Write minimal, effective tests**
|
||
|
- Use system tests sparingly as they increase the time to complete the test suite
|
||
|
- Only write tests for critical and important code paths
|
||
|
- Write tests as you go, when required
|
||
|
- Take a practical approach to testing. Tests are effective when their presence _significantly increases confidence in the codebase_.
|
||
|
|
||
|
Below are examples of necessary vs. unnecessary tests:
|
||
|
|
||
|
```rb
|
||
|
# GOOD!!
|
||
|
# Necessary test - in this case, we're testing critical domain business logic
|
||
|
test "syncs balances" do
|
||
|
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
|
||
|
|
||
|
@account.expects(:start_date).returns(2.days.ago.to_date)
|
||
|
|
||
|
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
|
||
|
[
|
||
|
Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
|
||
|
Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
|
||
|
]
|
||
|
)
|
||
|
|
||
|
assert_difference "@account.balances.count", 2 do
|
||
|
Balance::Syncer.new(@account, strategy: :forward).sync_balances
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# BAD!!
|
||
|
# Unnecessary test - in this case, this is simply testing ActiveRecord's functionality
|
||
|
test "saves balance" do
|
||
|
balance_record = Balance.new(balance: 100, currency: "USD")
|
||
|
|
||
|
assert balance_record.save
|
||
|
end
|
||
|
```
|
||
|
|
||
|
- **Test boundaries correctly**
|
||
|
- Distinguish between commands and query methods. Test output of query methods; test that commands were called with the correct params. See an example below:
|
||
|
|
||
|
```rb
|
||
|
class ExampleClass
|
||
|
def do_something
|
||
|
result = 2 + 2
|
||
|
|
||
|
CustomEventProcessor.process_result(result)
|
||
|
|
||
|
result
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class ExampleClass < ActiveSupport::TestCase
|
||
|
test "boundaries are tested correctly" do
|
||
|
result = ExampleClass.new.do_something
|
||
|
|
||
|
# GOOD - we're only testing that the command was received, not internal implementation details
|
||
|
# The actual tests for CustomEventProcessor belong in a different test suite!
|
||
|
CustomEventProcessor.expects(:process_result).with(4).once
|
||
|
|
||
|
# GOOD - we're testing the implementation of ExampleClass inside its own test suite
|
||
|
assert_equal 4, result
|
||
|
end
|
||
|
end
|
||
|
```
|
||
|
|
||
|
- Never test the implementation details of one class in another classes test suite
|
||
|
|
||
|
- **Stubs and mocks**
|
||
|
- Use `mocha` gem
|
||
|
- Always prefer `OpenStruct` when creating mock instances, or in complex cases, a mock class
|
||
|
- Only mock what's necessary. If you're not testing return values, don't mock a return value.
|
||
|
|
||
|
|