1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
# Shared Test Behaviors and Fixtures
This guide explains how to use shared test contexts and fixtures in sus to reduce duplication and ensure consistent test behavior across your test suite.
## Overview
When you have common test behaviors that need to be applied to multiple test files or multiple implementations of the same interface, shared contexts allow you to define those behaviors once and reuse them. This reduces duplication, ensures consistency, and makes it easier to maintain your tests.
Use shared contexts when you need:
- **Code reuse**: Apply the same test behavior to multiple classes or modules
- **Consistency**: Ensure all implementations of an interface are tested the same way
- **Maintainability**: Update test behavior in one place rather than many
- **Parameterization**: Run the same tests with different inputs or configurations
Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
When you have common test behaviors that you want to apply to multiple test files, add them to the `fixtures/` directory. When you have common test behaviors that you want to apply to multiple implementations of the same interface, within a single test file, you can define them as shared contexts within that file.
## Shared Fixtures
### Directory Structure
Shared fixtures are stored in the `fixtures/` directory, which mirrors your project structure:
```
my-gem/
├── lib/
│ ├── my_gem.rb
│ └── my_gem/
│ └── my_thing.rb
├── fixtures/
│ └── my_gem/
│ └── a_thing.rb # Provides MyGem::AThing shared context
└── test/
├── my_gem.rb
└── my_gem/
└── my_thing.rb
```
The `fixtures/` directory is automatically added to the `$LOAD_PATH`, so you can require files from there without needing to specify the full path.
### Creating Shared Fixtures
Create shared behaviors in the `fixtures/` directory using `Sus::Shared`:
```ruby
# fixtures/my_gem/a_user.rb
require "sus/shared"
module MyGem
AUser = Sus::Shared("a user") do |role|
let(:user) do
{
name: "Test User",
email: "test@example.com",
role: role
}
end
it "has a name" do
expect(user[:name]).not.to be_nil
end
it "has a valid email" do
expect(user[:email]).to be(:include?, "@")
end
it "has a role" do
expect(user[:role]).to be_a(String)
end
end
end
```
### Using Shared Fixtures
Require and use shared fixtures in your test files:
```ruby
# test/my_gem/user_manager.rb
require "my_gem/a_user"
describe MyGem::UserManager do
it_behaves_like MyGem::AUser, "manager"
# or include_context MyGem::AUser, "manager"
end
```
### Multiple Shared Fixtures
You can create multiple shared fixtures for different scenarios:
```ruby
# fixtures/my_gem/users.rb
module MyGem
module Users
AStandardUser = Sus::Shared("a standard user") do
let(:user) do
{ name: "John Doe", role: "user", active: true }
end
it "is active" do
expect(user[:active]).to be_truthy
end
end
AnAdminUser = Sus::Shared("an admin user") do
let(:user) do
{ name: "Admin User", role: "admin", active: true }
end
it "has admin role" do
expect(user[:role]).to be == "admin"
end
end
end
end
```
Use specific shared fixtures:
```ruby
# test/my_gem/authorization.rb
require "my_gem/users"
describe MyGem::Authorization do
with "standard user" do
# If there are no arguments, you can use `include` directly:
include MyGem::Users::AStandardUser
it "denies admin access" do
auth = subject.new
expect(auth.can_admin?(user)).to be_falsey
end
end
with "admin user" do
include MyGem::Users::AnAdminUser
it "allows admin access" do
auth = subject.new
expect(auth.can_admin?(user)).to be_truthy
end
end
end
```
### Modules
You can also define shared behaviors in modules and include them in your test files:
```ruby
# fixtures/my_gem/shared_behaviors.rb
module MyGem
module SharedBehaviors
def self.included(base)
base.it "uses shared data" do
expect(shared_data).to be == "some shared data"
end
end
def shared_data
"some shared data"
end
end
end
```
### Enumerating Tests
Some tests will be run multiple times with different arguments (for example, multiple database adapters). You can use `Sus::Shared` to define these tests and then enumerate them:
```ruby
# test/my_gem/database_adapter.rb
require "sus/shared"
ADatabaseAdapter = Sus::Shared("a database adapter") do |adapter|
let(:database) {adapter.new}
it "connects to the database" do
expect(database.connect).to be_truthy
end
it "can execute queries" do
expect(database.execute("SELECT 1")).to be == [[1]]
end
end
# Enumerate the tests with different adapters
MyGem::DatabaseAdapters.each do |adapter|
describe "with #{adapter}", unique: adapter.name do
it_behaves_like ADatabaseAdapter, adapter
end
end
```
Note the use of `unique: adapter.name` to ensure each test is uniquely identified, which is useful for reporting and debugging - otherwise the same test line number would be used for all iterations, which can make it hard to identify which specific test failed.
## Best Practices
1. **Organize by domain**: Group related shared contexts together in modules
2. **Keep contexts focused**: Each shared context should test one cohesive behavior
3. **Use parameters**: Make shared contexts flexible by accepting parameters
4. **Document intent**: Use clear names that explain what behavior is being tested
## Common Pitfalls
1. **Over-sharing**: Don't create shared contexts for behaviors that are only used once
2. **Tight coupling**: Avoid shared contexts that depend on too many specific implementation details
3. **Unclear names**: Use descriptive names that make it obvious what behavior is being tested
|