File: mocking.md

package info (click to toggle)
ruby-sus 0.35.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 380 kB
  • sloc: ruby: 2,844; makefile: 4
file content (165 lines) | stat: -rw-r--r-- 5,762 bytes parent folder | download
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
# Mocking

This guide explains how to use mocking in sus to isolate dependencies and verify interactions in your tests.

## Overview

When testing code that depends on external services, slow operations, or complex objects, you need a way to control those dependencies without actually invoking them. Mocking allows you to replace method implementations or set expectations on method calls, making your tests faster, more reliable, and easier to maintain.

Use mocking when you need:
- **Isolation**: Test your code without depending on external services (databases, APIs, file systems)
- **Performance**: Avoid slow operations during testing
- **Control**: Simulate error conditions or edge cases that are hard to reproduce
- **Verification**: Ensure your code calls methods with the correct arguments

Sus provides two types of mocking: `receive` for method call expectations and `mock` for replacing method implementations. 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.

**Important**: 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.

Sus does not support the concept of test doubles, but you can use `receive` and `mock` to achieve similar functionality.

## Method Call Expectations

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.

### Basic Usage

Verify that a method is called:

```ruby
describe PaymentProcessor do
	let(:payment_processor) {subject.new}
	let(:logger) {Object.new}
	
	it "logs payment attempts" do
		expect(logger).to receive(:info)
		
		payment_processor.process_payment(amount: 100, logger: logger)
	end
end
```

### With Arguments

Verify method calls with specific arguments:

```ruby
describe EmailService do
	let(:email_service) {subject.new}
	let(:smtp_client) {Object.new}
	
	it "sends emails with correct recipient and subject" do
		expect(smtp_client).to receive(:send).with("user@example.com", "Welcome!")
		
		email_service.send_welcome_email("user@example.com", smtp_client)
	end
end
```

You can also use more flexible argument matching:
- `.with_arguments(be == [arg1, arg2])` for positional arguments
- `.with_options(be == {option1: value1})` for keyword arguments
- `.with_block` to verify a block is passed

### Returning Values

Set up return values for mocked methods:

```ruby
describe UserRepository do
	let(:repository) {subject.new}
	let(:database) {Object.new}
	
	it "retrieves user by ID" do
		expected_user = {id: 1, name: "Alice"}
		expect(database).to receive(:find_user).with(1).and_return(expected_user)
		
		user = repository.find(1, database)
		expect(user).to be == expected_user
	end
end
```

### Raising Exceptions

Simulate error conditions:

```ruby
describe FileUploader do
	let(:uploader) {subject.new}
	let(:storage_service) {Object.new}
	
	it "handles storage failures gracefully" do
		expect(storage_service).to receive(:upload).and_raise(StandardError, "Storage unavailable")
		
		expect{uploader.upload_file("data.txt", storage_service)}.to raise_exception(StandardError, message: "Storage unavailable")
	end
end
```

### Multiple Calls

Verify methods are called multiple times:

```ruby
describe CacheWarmer do
	let(:warmer) {subject.new}
	let(:cache) {Object.new}
	
	it "warms multiple cache entries" do
		expect(cache).to receive(:set).twice.and_return(true)
		
		warmer.warm(["key1", "key2"], cache)
	end
end
```

You can also use `.with_call_count(be == 2)` for more flexible call count expectations.

## Mock Objects

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.

### Replacing Method Implementations

Replace methods to return controlled values:

```ruby
describe ApiClient do
	let(:http_client) {Object.new}
	let(:client) {ApiClient.new(http_client)}
	let(:users) {["Alice", "Bob"]}
	
	it "fetches users from API" do
		mock(http_client) do |mock|
			mock.replace(:get) do |url, headers: {}|
				expect(url).to be == "/api/users"
				expect(headers).to be == {"accept" => "application/json"}
				users.to_json
			end
		end
		
		expect(client.fetch_users).to be == users
	end
end
```

### Advanced Mocking Patterns

You can also use:
- `mock.before {|...| ...}` to execute code before the original method
- `mock.after {|...| ...}` to execute code after the original method
- `mock.wrap(:method) {|original, ...| original.call(...)}` to wrap the original method

## Best Practices

1. **Prefer real objects**: Use mocks only when necessary (external services, slow operations, error conditions)
2. **Use dependency injection**: Make dependencies explicit so they can be easily mocked
3. **Mock at boundaries**: Mock external services, not internal implementation details
4. **Keep mocks simple**: Complex mock setups indicate the code might need refactoring

## Common Pitfalls

1. **Over-mocking**: Mocking too much makes tests brittle and less valuable
2. **Thread safety**: Mock objects are thread-local, don't use them in multi-threaded tests
3. **Permanent changes**: Mocking non-local objects permanently changes their ancestors - use `let` for local objects instead