- Do not abbreviate things like
<name>
to<n>
. This is of the highest priority. Cursor will get this wrong every time and can't recover from it. - DO NOT ABBREVIATE
<name>
to<n>
EVER - I've added these here because Cursor will go down this rabbit hole of going back and forth on how to signify arguments in help text and then loses track. It takes me longer than I like to manually fix it.
- When I ask you to write a git commit message, please run
git diff --cached | cat
to see what's staged and write a message for that change
Coding¶
- Reduce the use of conditional logic and prefer pattern matching on functions
- Prefer pipelines when possible using the
|>
operator - Before coding, make sure we understand the feature at a high-level. It helps to create new documentation while discussing the feature.
- Try to keep each coding session focused on one thing at a time. It's easy to let things baloon out of control
- Always minimize work in progress
- Follow the single responsibility principle when you can. Each module should have a clear responsibility. Consider extracting code to a new module when it becomes confusing.
- Always fix the warnings before we move on
Documentation¶
- After each bit of coding, consider how the documentation needs to be updated. It's very easy for documentation to get stale. Documentation should be ever green, reflecting our latest designs.
- Try to keep as much implementation logic out of the documentation as possible. Keep the documentation at a high-level.
- Documentation should be something a human (or AI) can read and understand quickly. Over documenting might be as bad as under documenting.
Data Types¶
- All Discord IDs (guild_id, user_id, channel_id, message_id) must be:
- Defined as
:integer
in schemas - Stored as
:bigint
in the database - This ensures consistency across the codebase and handles Discord's large ID numbers
Testing¶
Basic TDD¶
- Write tests before you write code to demonstrate how you expect the code to work
- See the tests fail appropriately. That means the error should indicate the missing behavior, not something related to the test set up.
- Implement only the minimum amount of code to make the test pass. This could mean hard-coding return values.
- New tests should drive the implementation to be more complete
- Once all the tests pass, review the code and refactor to be as idiomatic and simple as possible
Specific Practices¶
- Avoid mocking when we can. Mocking can lead to a false sense of security. Over mocking can cause tests to pass when the implementation is doing something else.
- Do not mock code we own. It's better to call it than mock it.
- Prefer calling actual code to setup test data. Ussing
Character.parse/1
to setup characters is much better than trying to manually construct a%Character{}
struct. There is a lot of setup code inCharacter.parse/1
that's import to getting valid data. - Dependency injection with test implementations is better than mocking. Multiple airity for functions in elixir should make this relatively easy to implement
- When you run all the tests with
mix test
and they pass with no failures and no warnings, there's no need to re-run individual tests - Typically, just run the tests for the code we're working on. As long as their are errors or warnings, focus on fixing those and running the specific tests. Run the tests for the whole file. Trying to pick out the specific line number is challenging. After the whole test file runs without failure or warnings, run the whole test suite with
mix test
. - Always generate migrations with
mix ecto.gen.migration
. This ensures the timestamp in the filename is correct and that migration filenames always increase in value
Test Helpers¶
We provide several test helper modules to simplify testing external dependencies without mocking. These helpers follow our dependency injection approach and maintain a consistent interface with the real modules.
Message API Helpers¶
For testing commands that send Discord messages:
Stingbatbot.Test.MessageApi
- Captures sent messages for verification. Provides functions likeget_last_message/1
andclear_messages/0
.Stingbatbot.Test.ChannelApi
Stingbatbot.Test.ErrorMessageApi
- Simulates message sending failures.Stingbatbot.Test.MessageApiHelper
- Creates customized message API test implementations.
Example: Instead of mocking the MessageApi, inject our test implementation:
# Pass the test message API to the function
result = Command.execute(context, message_api: MessageApi)
# Verify the message that was sent
assert MessageApi.get_last_message(channel_id) =~ "Expected content"
HTTP Client Helpers¶
For testing commands that make HTTP requests:
Stingbatbot.Test.JSONHTTPClient
- Returns configurable JSON responses.Stingbatbot.Test.ErrorHTTPClient
- Simulates HTTP request failures.Stingbatbot.Test.StatusHTTPClient
- Simulates specific HTTP status codes.Stingbatbot.Test.HTTPClientHelper
- Factory for creating HTTP client test implementations.
Game Data Helpers¶
For testing with consistent game data fixtures:
Stingbatbot.Test.WeaponHelper
- Creates realistic weapon fixtures for tests. Provides functions to:get_weapon_json_by_name/1
: Get the raw JSON data for a weapon from the WeaponStorecreate_weapon_inventory_item/2
: Create a character inventory item with proper weapon structurecreate_weapon_inventory_items/1
: Create multiple weapon inventory items from a list of names
Example: Create test fixtures that match production data structures:
# Create a character with realistic weapons
character_data = %{
"gear" => WeaponHelper.create_weapon_inventory_items(["Longsword", "Dagger"])
}
# Create a single weapon with a specific instance ID
longsword = WeaponHelper.create_weapon_inventory_item("Longsword", instance_id: "test123")
Using these helpers makes tests more realistic and avoids mocking code we own.
Character Factory¶
The Stingbatbot.Test.CharacterFactory
module provides a consistent way to create character test data. It should be used instead of manually constructing character data structures.
Basic Usage¶
# Create a basic character
json = CharacterFactory.create_character_json()
# Create a customized character
custom_json = CharacterFactory.create_character_json(
name: "Gund",
class: "Fighter",
level: 3,
stats: %{"STR" => 16},
weapons: ["Greatsword", "Dagger"]
)
Class-Specific Factories¶
Use the class-specific factory functions for characters of specific classes:
# Create a fighter
fighter_json = CharacterFactory.create_fighter_json()
# Create a wizard with spells
wizard_json = CharacterFactory.create_wizard_json(
spells: ["Fireball", "Magic Missile"]
)
# Create a priest with talents
priest_json = CharacterFactory.create_priest_json(
talents: [
%{
"sourceType" => "Class",
"sourceName" => "Priest",
"sourceCategory" => "Talent",
"name" => "Plus1ToCastingSpells",
"bonusName" => "Plus1ToCastingSpells",
"bonusTo" => "Priest",
"gainedAtLevel" => 3
}
]
)
Best Practices¶
- Use Factory Functions: Always use factory functions instead of manually constructing character data
- Start with Base: Use
create_character_json()
as the base for custom characters - Class-Specific: Use class-specific factories when testing class-specific features
- Combine Bonuses: Use the
bonuses
option to add custom bonuses or talents - Real Data: The factory uses real data from
WeaponStore
andSpellList
, ensuring test data matches production - Use minimal specification: The factory has good defaults. Only pass in attributes that are really important for the specific test. Adding extra attributes makes the tests harder to read and harder to know what's important.
For more examples, see test/stingbatbot/character_factory_example_test.exs
.