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
|