Skip to content

Commit 1c38802

Browse files
committed
Add agent context.
1 parent 02bc976 commit 1c38802

File tree

5 files changed

+663
-0
lines changed

5 files changed

+663
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/gems.locked
44
/.covered.db
55
/external
6+
/.context

context/mocking.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Mocking
2+
3+
There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace method implementations or set up more complex behavior.
4+
5+
Mocking non-local objects permanently changes the object's ancestors, so it should be used with care. For local objects, you can use `let` to define the object and then mock it.
6+
7+
Sus does not support the concept of test doubles, but you can use `receive` and `mock` to achieve similar functionality.
8+
9+
## Method Call Expectations
10+
11+
The `receive(:method)` expectation is used to set up an expectation that a method will be called on an object. You can also specify arguments and return values. However, `receive` is not sequenced, meaning it does not enforce the order of method calls. If you need to enforce the order, use `mock` instead.
12+
13+
```ruby
14+
describe MyThing do
15+
let(:my_thing) {subject.new}
16+
17+
it "calls the expected method" do
18+
expect(my_thing).to receive(:my_method)
19+
20+
expect(my_thing.my_method).to be == 42
21+
end
22+
end
23+
```
24+
25+
### With Arguments
26+
27+
```ruby
28+
it "calls the method with arguments" do
29+
expect(object).to receive(:method_name).with(arg1, arg2)
30+
# or .with_arguments(be == [arg1, arg2])
31+
# or .with_options(be == {option1: value1, option2: value2})
32+
# or .with_block
33+
34+
object.method_name(arg1, arg2)
35+
end
36+
```
37+
38+
### Returning Values
39+
40+
```ruby
41+
it "returns a value" do
42+
expect(object).to receive(:method_name).and_return("expected value")
43+
result = object.method_name
44+
expect(result).to be == "expected value"
45+
end
46+
```
47+
48+
### Raising Exceptions
49+
50+
```ruby
51+
it "raises an exception" do
52+
expect(object).to receive(:method_name).and_raise(StandardError, "error message")
53+
54+
expect{object.method_name}.to raise_exception(StandardError, message: "error message")
55+
end
56+
```
57+
58+
### Multiple Calls
59+
60+
```ruby
61+
it "calls the method multiple times" do
62+
expect(object).to receive(:method_name).twice.and_return("result")
63+
# or .with_call_count(be == 2)
64+
expect(object.method_name).to be == "result"
65+
expect(object.method_name).to be == "result"
66+
end
67+
```
68+
69+
## Mock Objects
70+
71+
Mock objects are used to replace method implementations or set up complex behavior. They can be used to intercept method calls, modify arguments, and control the flow of execution. They are thread-local, meaning they only affect the current thread, therefore are not suitable for use in tests that have multiple threads.
72+
73+
```ruby
74+
describe ApiClient do
75+
let(:http_client) {Object.new}
76+
let(:client) {ApiClient.new(http_client)}
77+
let(:users) {["Alice", "Bob"]}
78+
79+
it "makes GET requests" do
80+
mock(http_client) do |mock|
81+
mock.replace(:get) do |url, headers: {}|
82+
expect(url).to be == "/api/users"
83+
expect(headers).to be == {"accept" => "application/json"}
84+
users.to_json
85+
end
86+
87+
# or mock.before {|...| ...}
88+
# or mock.after {|...| ...}
89+
# or mock.wrap(:new) {|original, ...| original.call(...)}
90+
end
91+
92+
expect(client.fetch_users).to be == users
93+
end
94+
end
95+
```

context/shared.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Shared Test Behaviors and Fixtures
2+
3+
## Overview
4+
5+
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.
6+
7+
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.
8+
9+
## Shared Fixtures
10+
11+
### Directory Structure
12+
13+
```
14+
my-gem/
15+
├── lib/
16+
│ ├── my_gem.rb
17+
│ └── my_gem/
18+
│ └── my_thing.rb
19+
├── fixtures/
20+
│ └── my_gem/
21+
│ └── a_thing.rb # Provides MyGem::AThing shared context
22+
└── test/
23+
├── my_gem.rb
24+
└── my_gem/
25+
└── my_thing.rb
26+
```
27+
28+
### Creating Shared Fixtures
29+
30+
Create shared behaviors in the `fixtures/` directory using `Sus::Shared`:
31+
32+
```ruby
33+
# fixtures/my_gem/a_user.rb
34+
35+
require "sus/shared"
36+
37+
module MyGem
38+
AUser = Sus::Shared("a user") do |role|
39+
let(:user) do
40+
{
41+
name: "Test User",
42+
43+
role: role
44+
}
45+
end
46+
47+
it "has a name" do
48+
expect(user[:name]).not.to be_nil
49+
end
50+
51+
it "has a valid email" do
52+
expect(user[:email]).to be(:include?, "@")
53+
end
54+
55+
it "has a role" do
56+
expect(user[:role]).to be_a(String)
57+
end
58+
end
59+
end
60+
```
61+
62+
### Using Shared Fixtures
63+
64+
Require and use shared fixtures in your test files:
65+
66+
```ruby
67+
# test/my_gem/user_manager.rb
68+
require 'my_gem/a_user'
69+
70+
describe MyGem::UserManager do
71+
it_behaves_like MyGem::AUser, "manager"
72+
# or include_context MyGem::AUser, "manager"
73+
end
74+
```
75+
76+
### Multiple Shared Fixtures
77+
78+
You can create multiple shared fixtures for different scenarios:
79+
80+
```ruby
81+
# fixtures/my_gem/users.rb
82+
module MyGem
83+
module Users
84+
AStandardUser = Sus::Shared("a standard user") do
85+
let(:user) do
86+
{ name: "John Doe", role: "user", active: true }
87+
end
88+
89+
it "is active" do
90+
expect(user[:active]).to be_truthy
91+
end
92+
end
93+
94+
AnAdminUser = Sus::Shared("an admin user") do
95+
let(:user) do
96+
{ name: "Admin User", role: "admin", active: true }
97+
end
98+
99+
it "has admin role" do
100+
expect(user[:role]).to be == "admin"
101+
end
102+
end
103+
end
104+
end
105+
```
106+
107+
Use specific shared fixtures:
108+
109+
```ruby
110+
# test/my_gem/authorization.rb
111+
require 'my_gem/users'
112+
113+
describe MyGem::Authorization do
114+
with "standard user" do
115+
# If there are no arguments, you can use `include` directly:
116+
include MyGem::Users::AStandardUser
117+
118+
it "denies admin access" do
119+
auth = subject.new
120+
expect(auth.can_admin?(user)).to be_falsey
121+
end
122+
end
123+
124+
with "admin user" do
125+
include MyGem::Users::AnAdminUser
126+
127+
it "allows admin access" do
128+
auth = subject.new
129+
expect(auth.can_admin?(user)).to be_truthy
130+
end
131+
end
132+
end
133+
```
134+
135+
### Modules
136+
137+
You can also define shared behaviors in modules and include them in your test files:
138+
139+
```ruby
140+
# fixtures/my_gem/shared_behaviors.rb
141+
module MyGem
142+
module SharedBehaviors
143+
def self.included(base)
144+
base.it "uses shared data" do
145+
expect(shared_data).to be == "some shared data"
146+
end
147+
end
148+
149+
def shared_data
150+
"some shared data"
151+
end
152+
end
153+
end
154+
```
155+
156+
### Enumerating Tests
157+
158+
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:
159+
160+
```ruby
161+
# test/my_gem/database_adapter.rb
162+
163+
require "sus/shared"
164+
165+
ADatabaseAdapter = Sus::Shared("a database adapter") do |adapter|
166+
let(:database) {adapter.new}
167+
168+
it "connects to the database" do
169+
expect(database.connect).to be_truthy
170+
end
171+
172+
it "can execute queries" do
173+
expect(database.execute("SELECT 1")).to be == [[1]]
174+
end
175+
end
176+
177+
# Enumerate the tests with different adapters
178+
MyGem::DatabaseAdapters.each do |adapter|
179+
describe "with #{adapter}", unique: adapter.name do
180+
it_behaves_like ADatabaseAdapter, adapter
181+
end
182+
end
183+
```
184+
185+
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.

0 commit comments

Comments
 (0)