interaktor 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +30 -0
- data/.github/workflows/tests.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +1 -12
- data/Gemfile +3 -2
- data/Guardfile +42 -0
- data/README.md +87 -132
- data/interaktor.gemspec +1 -1
- data/lib/interaktor.rb +12 -188
- data/lib/interaktor/callable.rb +219 -0
- data/lib/interaktor/context.rb +16 -1
- data/lib/interaktor/error/attribute_error.rb +16 -0
- data/lib/interaktor/error/base.rb +9 -0
- data/lib/interaktor/error/disallowed_attribute_assignment_error.rb +9 -0
- data/lib/interaktor/error/missing_attribute_error.rb +5 -0
- data/lib/interaktor/error/option_error.rb +16 -0
- data/lib/interaktor/error/unknown_attribute_error.rb +5 -0
- data/lib/interaktor/error/unknown_option_error.rb +5 -0
- data/lib/interaktor/organizer.rb +7 -20
- data/spec/integration_spec.rb +18 -15
- data/spec/interactor/organizer_spec.rb +81 -13
- data/spec/spec_helper.rb +8 -3
- data/spec/support/lint.rb +289 -57
- metadata +19 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be48f704b8d52c81c07e9708d1e90230ead270e4da3ddf74e8e4e6251e677978
|
4
|
+
data.tar.gz: '09e99c6f8129b44ff287aa9e8a742327cac3a232f13d7596ece9b428a575bc28'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea1fe86854da930c537f9fb69864accdf8cb1948d8dacae4b64685ff88508964e99eb968826a12e5653791f492c1e8af6426b734838c5ac152c581dfee6399ba
|
7
|
+
data.tar.gz: 0d8c4406b6d3ee81f39c6adc56118ce4e204465743eed6dc5f342f3b3b5a9a03722f9b04ed48dab265ef9bfbcb54e047476245fdf000295b150e2a835f8087e6
|
@@ -0,0 +1,30 @@
|
|
1
|
+
name: Publish
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [master]
|
6
|
+
tags:
|
7
|
+
- v*
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
build:
|
11
|
+
name: Build and publish
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v2
|
16
|
+
- name: Set up Ruby
|
17
|
+
uses: actions/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: 2.5.x
|
20
|
+
|
21
|
+
- name: Publish to RubyGems
|
22
|
+
run: |
|
23
|
+
mkdir -p $HOME/.gem
|
24
|
+
touch $HOME/.gem/credentials
|
25
|
+
chmod 0600 $HOME/.gem/credentials
|
26
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
27
|
+
gem build *.gemspec
|
28
|
+
gem push *.gem
|
29
|
+
env:
|
30
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: Tests
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
os: [ubuntu, macos]
|
11
|
+
ruby: [2.5, 2.6, 2.7, head, debug]
|
12
|
+
runs-on: ${{ matrix.os }}-latest
|
13
|
+
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v2
|
16
|
+
- name: Set up Ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby }}
|
20
|
+
bundler-cache: true
|
21
|
+
- name: Install dependencies
|
22
|
+
run: bundle install
|
23
|
+
- name: Run tests
|
24
|
+
run: bundle exec rspec
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.8
|
data/.travis.yml
CHANGED
@@ -1,25 +1,14 @@
|
|
1
|
-
after_success:
|
2
|
-
- bundle exec codeclimate-test-reporter
|
3
1
|
before_install:
|
4
2
|
- gem update bundler rake
|
5
|
-
branches:
|
6
|
-
only:
|
7
|
-
- master
|
8
|
-
- v4
|
9
3
|
cache: bundler
|
10
4
|
language: ruby
|
11
5
|
matrix:
|
12
6
|
allow_failures:
|
13
7
|
- rvm: ruby-head
|
14
|
-
notifications:
|
15
|
-
webhooks:
|
16
|
-
on_start: always
|
17
|
-
urls:
|
18
|
-
- http://buildlight.collectiveidea.com/
|
19
8
|
rvm:
|
20
9
|
- "2.5"
|
21
10
|
- "2.6"
|
22
11
|
- "2.7"
|
23
12
|
- ruby-head
|
24
|
-
script: bundle exec
|
13
|
+
script: bundle exec rspec
|
25
14
|
sudo: false
|
data/Gemfile
CHANGED
@@ -7,9 +7,10 @@ gem "rubocop-performance"
|
|
7
7
|
gem "rubocop-rspec"
|
8
8
|
gem "rufo", "~> 0.12.0"
|
9
9
|
gem "solargraph"
|
10
|
+
gem "guard-rspec", require: false
|
10
11
|
|
11
12
|
group :test do
|
12
|
-
gem "
|
13
|
-
gem "pry-byebug"
|
13
|
+
gem "pry-byebug", platforms: [:mri]
|
14
14
|
gem "rspec", "~> 3.9.0"
|
15
|
+
gem "simplecov"
|
15
16
|
end
|
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
end
|
data/README.md
CHANGED
@@ -3,21 +3,23 @@
|
|
3
3
|
[![Gem Version](https://img.shields.io/gem/v/interaktor.svg)](http://rubygems.org/gems/interaktor)
|
4
4
|
[![Build Status](https://img.shields.io/travis/collectiveidea/interaktor/master.svg)](https://travis-ci.org/taylorthurlow/interaktor)
|
5
5
|
|
6
|
-
**Interaktor
|
6
|
+
**DISCLAIMER: Interaktor is under active development. Feel free to use it, but until 1.0 is released, any update could break compatibility with an older version.**
|
7
7
|
|
8
|
-
|
8
|
+
**Interaktor** is a fork of [Interaktor by collectiveidea](https://github.com/collectiveidea/interaktor). While Interactor is still used by collectiveidea internally, communication and progress has been slow in adapting to pull requests and issues. This inactivity combined with my desire to dial back on the Interactor's inherent permissivity led me to fork it and create Interaktor.
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
- Attributes required
|
13
|
-
-
|
10
|
+
Fundamentally, Interaktor is the same as Interactor, but with the following changes:
|
11
|
+
|
12
|
+
- Required explicit definition of interaktor "attributes" which replaces the concept of the interaktor context. Attributes can be required or optional, and support options like default values.
|
13
|
+
- The interaktor "context" is no longer a public-facing concept, all data/attribute accessors/setters are defined as attributes
|
14
|
+
- Attributes passed to `#fail!` must be defined in advance
|
15
|
+
- Interaktors support early-exit functionality through the use of `#success!`, which functions the same as `#fail!` in that you must define the required success attributes on the interaktor
|
14
16
|
|
15
17
|
## Getting started
|
16
18
|
|
17
19
|
Add `interaktor` to your Gemfile and `bundle install`.
|
18
20
|
|
19
21
|
```ruby
|
20
|
-
gem "interaktor"
|
22
|
+
gem "interaktor"
|
21
23
|
```
|
22
24
|
|
23
25
|
## What is an interaktor?
|
@@ -90,13 +92,11 @@ else
|
|
90
92
|
end
|
91
93
|
```
|
92
94
|
|
93
|
-
#### Dealing with
|
94
|
-
|
95
|
-
`context.fail!` always throws an exception of type `Interaktor::Failure`.
|
95
|
+
#### Dealing with failure
|
96
96
|
|
97
|
-
|
97
|
+
`#fail!` always throws an exception of type `Interaktor::Failure`.
|
98
98
|
|
99
|
-
This works because the `call` class method swallows exceptions. When unit testing an interaktor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
|
99
|
+
Normally, however, these exceptions are not seen. In the recommended usage, the caller invokes the interaktor using the class method `.call`, then checks the `#success?` method of the returned object. This works because the `call` class method swallows exceptions. When unit testing an interaktor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
|
100
100
|
|
101
101
|
See _Interaktors in the controller_, below, for the recommended usage of `call` and `success?`.
|
102
102
|
|
@@ -104,7 +104,7 @@ See _Interaktors in the controller_, below, for the recommended usage of `call`
|
|
104
104
|
|
105
105
|
#### Before hooks
|
106
106
|
|
107
|
-
Sometimes an interaktor needs to prepare
|
107
|
+
Sometimes an interaktor needs to prepare something before the interaktor is even run. This can be done with before hooks on the interaktor.
|
108
108
|
|
109
109
|
```ruby
|
110
110
|
before do
|
@@ -124,12 +124,11 @@ end
|
|
124
124
|
|
125
125
|
#### After hooks
|
126
126
|
|
127
|
-
Interaktors can also perform teardown operations after the interaktor instance
|
128
|
-
is run. They are only run on success.
|
127
|
+
Interaktors can also perform teardown operations after the interaktor instance is run. They are only run on success.
|
129
128
|
|
130
129
|
```ruby
|
131
130
|
after do
|
132
|
-
|
131
|
+
user.reload
|
133
132
|
end
|
134
133
|
```
|
135
134
|
|
@@ -161,9 +160,7 @@ If `#fail!` is called, any code defined in the hook after the call to the intera
|
|
161
160
|
|
162
161
|
#### Hook sequence
|
163
162
|
|
164
|
-
Before hooks are invoked in the order in which they were defined while after
|
165
|
-
hooks are invoked in the opposite order. Around hooks are invoked outside of any
|
166
|
-
defined before and after hooks. For example:
|
163
|
+
Before hooks are invoked in the order in which they were defined while after hooks are invoked in the opposite order. Around hooks are invoked outside of any defined before and after hooks. For example:
|
167
164
|
|
168
165
|
```ruby
|
169
166
|
around do |interaktor|
|
@@ -210,8 +207,7 @@ around after 1
|
|
210
207
|
|
211
208
|
#### Interaktor concerns
|
212
209
|
|
213
|
-
An interaktor can define multiple before/after hooks, allowing common hooks to
|
214
|
-
be extracted into interaktor concerns.
|
210
|
+
An interaktor can define multiple before/after hooks, allowing common hooks to be extracted into interaktor concerns.
|
215
211
|
|
216
212
|
```ruby
|
217
213
|
module InteraktorDoStuff
|
@@ -227,12 +223,9 @@ module InteraktorDoStuff
|
|
227
223
|
end
|
228
224
|
```
|
229
225
|
|
230
|
-
# All documentation below this line has not been updated to reflect the fork from Interactor.
|
231
|
-
|
232
226
|
## Kinds of interaktors
|
233
227
|
|
234
|
-
There are two kinds of interaktors built into the Interaktor library: basic
|
235
|
-
interaktors and organizers.
|
228
|
+
There are two kinds of interaktors built into the Interaktor library: basic interaktors and organizers.
|
236
229
|
|
237
230
|
### Interaktors
|
238
231
|
|
@@ -242,35 +235,41 @@ A basic interaktor is a class that includes `Interaktor` and defines `call`.
|
|
242
235
|
class AuthenticateUser
|
243
236
|
include Interaktor
|
244
237
|
|
238
|
+
required :email
|
239
|
+
required :password
|
240
|
+
|
241
|
+
success :user
|
242
|
+
success :token
|
243
|
+
|
244
|
+
failure :message
|
245
|
+
|
245
246
|
def call
|
246
|
-
if user = User.authenticate(
|
247
|
-
|
248
|
-
context.token = user.secret_token
|
247
|
+
if user = User.authenticate(email, password)
|
248
|
+
success!(user: user, token: user.secret_token)
|
249
249
|
else
|
250
|
-
|
250
|
+
fail!(message: "authenticate_user.failure")
|
251
251
|
end
|
252
252
|
end
|
253
253
|
end
|
254
254
|
```
|
255
255
|
|
256
|
-
Basic interaktors are the building blocks. They are your application's
|
257
|
-
single-purpose units of work.
|
256
|
+
Basic interaktors are the building blocks. They are your application's single-purpose units of work.
|
258
257
|
|
259
258
|
### Organizers
|
260
259
|
|
261
|
-
An organizer is an important variation on the basic interaktor. Its single
|
262
|
-
purpose is to run _other_ interaktors.
|
260
|
+
An organizer is an important variation on the basic interaktor. Its single purpose is to run _other_ interaktors.
|
263
261
|
|
264
262
|
```ruby
|
265
263
|
class PlaceOrder
|
266
264
|
include Interaktor::Organizer
|
267
265
|
|
266
|
+
required :order_params
|
267
|
+
|
268
268
|
organize CreateOrder, ChargeCard, SendThankYou
|
269
269
|
end
|
270
270
|
```
|
271
271
|
|
272
|
-
In the controller, you can run the `PlaceOrder` organizer just like you would
|
273
|
-
any other interaktor:
|
272
|
+
In the controller, you can run the `PlaceOrder` organizer just like you would any other interaktor:
|
274
273
|
|
275
274
|
```ruby
|
276
275
|
class OrdersController < ApplicationController
|
@@ -293,69 +292,71 @@ class OrdersController < ApplicationController
|
|
293
292
|
end
|
294
293
|
```
|
295
294
|
|
296
|
-
The organizer passes its
|
297
|
-
time and in order. Each interaktor may change that context before it's passed
|
298
|
-
along to the next interaktor.
|
295
|
+
The organizer passes any of its own defined attributes into first interaktor that it organizes. That first interaktor is then called and executed using those attributes. For the following interaktors in the organize list, each interaktor receives its attributes from the previous interaktor (both input attributes and success attributes). Any attributes which are _not_ accepted by the next interaktor (listed as required or optional attributes) are dropped in the transition.
|
299
296
|
|
300
297
|
#### Rollback
|
301
298
|
|
302
|
-
If any one of the organized interaktors fails
|
303
|
-
If the `ChargeCard` interaktor fails, `SendThankYou` is never called.
|
299
|
+
If any one of the organized interaktors fails, the organizer stops. If the `ChargeCard` interaktor fails, `SendThankYou` is never called.
|
304
300
|
|
305
|
-
In addition, any interaktors that had already run are given the chance to undo
|
306
|
-
themselves, in reverse order. Simply define the `rollback` method on your
|
307
|
-
interaktors:
|
301
|
+
In addition, any interaktors that had already run are given the chance to undo themselves, in reverse order. Simply define the `rollback` method on your interaktors.
|
308
302
|
|
309
303
|
```ruby
|
310
304
|
class CreateOrder
|
311
305
|
include Interaktor
|
312
306
|
|
307
|
+
required :order_params
|
308
|
+
|
309
|
+
success :order
|
310
|
+
|
313
311
|
def call
|
314
312
|
order = Order.create(order_params)
|
315
313
|
|
316
314
|
if order.persisted?
|
317
|
-
|
315
|
+
success!(order: order)
|
318
316
|
else
|
319
|
-
|
317
|
+
fail!
|
320
318
|
end
|
321
319
|
end
|
322
320
|
|
323
321
|
def rollback
|
324
|
-
|
322
|
+
order.destroy
|
325
323
|
end
|
326
324
|
end
|
327
325
|
```
|
328
326
|
|
329
|
-
**NOTE:** The interaktor that fails is _not_ rolled back. Because every
|
330
|
-
interaktor should have a single purpose, there should be no need to clean up
|
331
|
-
after any failed interaktor.
|
327
|
+
**NOTE:** The interaktor that fails is _not_ rolled back. Because every interaktor should have a single purpose, there should be no need to clean up after any failed interaktor. This is why the rollback method above can access the `order` success attribute - rollback is only called on successful interaktors.
|
332
328
|
|
333
329
|
## Testing interaktors
|
334
330
|
|
335
|
-
When written correctly, an interaktor is easy to test because it only _does_ one
|
336
|
-
thing. Take the following interaktor:
|
331
|
+
When written correctly, an interaktor is easy to test because it only _does_ one thing. Take the following interaktor:
|
337
332
|
|
338
333
|
```ruby
|
339
334
|
class AuthenticateUser
|
340
335
|
include Interaktor
|
341
336
|
|
337
|
+
required :email
|
338
|
+
required :password
|
339
|
+
|
340
|
+
success :user
|
341
|
+
success :token
|
342
|
+
|
343
|
+
failure :message
|
344
|
+
|
342
345
|
def call
|
343
|
-
if user = User.authenticate(
|
344
|
-
|
345
|
-
context.token = user.secret_token
|
346
|
+
if user = User.authenticate(email, password)
|
347
|
+
success!(user: user, token: user.secret_token)
|
346
348
|
else
|
347
|
-
|
349
|
+
fail!(message: "authenticate_user.failure")
|
348
350
|
end
|
349
351
|
end
|
350
352
|
end
|
351
353
|
```
|
352
354
|
|
353
|
-
You can test just this interaktor's single purpose and how it affects the
|
354
|
-
context.
|
355
|
+
You can test just this interaktor's single purpose and how it affects the result.
|
355
356
|
|
356
357
|
```ruby
|
357
358
|
describe AuthenticateUser do
|
358
|
-
subject(:
|
359
|
+
subject(:result) { AuthenticateUser.call(email: "john@example.com", password: "secret") }
|
359
360
|
|
360
361
|
describe ".call" do
|
361
362
|
context "when given valid credentials" do
|
@@ -366,15 +367,15 @@ describe AuthenticateUser do
|
|
366
367
|
end
|
367
368
|
|
368
369
|
it "succeeds" do
|
369
|
-
expect(
|
370
|
+
expect(result).to be_a_success
|
370
371
|
end
|
371
372
|
|
372
373
|
it "provides the user" do
|
373
|
-
expect(
|
374
|
+
expect(result.user).to eq(user)
|
374
375
|
end
|
375
376
|
|
376
377
|
it "provides the user's secret token" do
|
377
|
-
expect(
|
378
|
+
expect(result.token).to eq("token")
|
378
379
|
end
|
379
380
|
end
|
380
381
|
|
@@ -384,80 +385,62 @@ describe AuthenticateUser do
|
|
384
385
|
end
|
385
386
|
|
386
387
|
it "fails" do
|
387
|
-
expect(
|
388
|
+
expect(result).to be_a_failure
|
388
389
|
end
|
389
390
|
|
390
391
|
it "provides a failure message" do
|
391
|
-
expect(
|
392
|
+
expect(result.message).to be_present
|
392
393
|
end
|
393
394
|
end
|
394
395
|
end
|
395
396
|
end
|
396
397
|
```
|
397
398
|
|
398
|
-
[We](http://collectiveidea.com) use RSpec but the same approach applies to any
|
399
|
-
testing framework.
|
400
|
-
|
401
399
|
### Isolation
|
402
400
|
|
403
|
-
You may notice that we stub `User.authenticate` in our test rather than creating
|
404
|
-
users in the database. That's because our purpose in
|
405
|
-
`spec/interaktors/authenticate_user_spec.rb` is to test just the
|
406
|
-
`AuthenticateUser` interaktor. The `User.authenticate` method is put through its
|
407
|
-
own paces in `spec/models/user_spec.rb`.
|
401
|
+
You may notice that we stub `User.authenticate` in our test rather than creating users in the database. That's because our purpose in `spec/interaktors/authenticate_user_spec.rb` is to test just the `AuthenticateUser` interaktor. The `User.authenticate` method is put through its own paces in `spec/models/user_spec.rb`.
|
408
402
|
|
409
|
-
It's a good idea to define your own interfaces to your models. Doing so makes it
|
410
|
-
easy to draw a line between which responsibilities belong to the interaktor and
|
411
|
-
which to the model. The `User.authenticate` method is a good, clear line.
|
412
|
-
Imagine the interaktor otherwise:
|
403
|
+
It's a good idea to define your own interfaces to your models. Doing so makes it easy to draw a line between which responsibilities belong to the interaktor and which to the model. The `User.authenticate` method is a good, clear line. Imagine the interaktor otherwise:
|
413
404
|
|
414
405
|
```ruby
|
415
406
|
class AuthenticateUser
|
416
407
|
include Interaktor
|
417
408
|
|
409
|
+
required :email
|
410
|
+
required :password
|
411
|
+
|
412
|
+
success :user
|
413
|
+
|
414
|
+
failure :message
|
415
|
+
|
418
416
|
def call
|
419
|
-
user = User.
|
417
|
+
user = User.find_by(email: email)
|
420
418
|
|
421
419
|
# Yuck!
|
422
|
-
if user && BCrypt::Password.new(user.password_digest) ==
|
423
|
-
|
420
|
+
if user && BCrypt::Password.new(user.password_digest) == password
|
421
|
+
success!(user: user)
|
424
422
|
else
|
425
|
-
|
423
|
+
fail!(message: "authenticate_user.failure")
|
426
424
|
end
|
427
425
|
end
|
428
426
|
end
|
429
427
|
```
|
430
428
|
|
431
|
-
It would be very difficult to test this interaktor in isolation and even if you
|
432
|
-
did, as soon as you change your ORM or your encryption algorithm (both model
|
433
|
-
concerns), your interaktors (business concerns) break.
|
429
|
+
It would be very difficult to test this interaktor in isolation and even if you did, as soon as you change your ORM or your encryption algorithm (both model concerns), your interaktors (business concerns) break.
|
434
430
|
|
435
431
|
_Draw clear lines._
|
436
432
|
|
437
433
|
### Integration
|
438
434
|
|
439
|
-
While it's important to test your interaktors in isolation, it's just as
|
440
|
-
important to write good integration or acceptance tests.
|
435
|
+
While it's important to test your interaktors in isolation, it's just as important to write good integration or acceptance tests.
|
441
436
|
|
442
|
-
One of the pitfalls of testing in isolation is that when you stub a method, you
|
443
|
-
could be hiding the fact that the method is broken, has changed or doesn't even
|
444
|
-
exist.
|
437
|
+
One of the pitfalls of testing in isolation is that when you stub a method, you could be hiding the fact that the method is broken, has changed or doesn't even exist.
|
445
438
|
|
446
|
-
When you write full-stack tests that tie all of the pieces together, you can be
|
447
|
-
sure that your application's individual pieces are working together as expected.
|
448
|
-
That becomes even more important when you add a new layer to your code like
|
449
|
-
interaktors.
|
450
|
-
|
451
|
-
**TIP:** If you track your test coverage, try for 100% coverage _before_
|
452
|
-
integrations tests. Then keep writing integration tests until you sleep well at
|
453
|
-
night.
|
439
|
+
When you write full-stack tests that tie all of the pieces together, you can be sure that your application's individual pieces are working together as expected. That becomes even more important when you add a new layer to your code like interaktors.
|
454
440
|
|
455
441
|
### Controllers
|
456
442
|
|
457
|
-
One of the advantages of using interaktors is how much they simplify controllers
|
458
|
-
and their tests. Because you're testing your interaktors thoroughly in isolation
|
459
|
-
as well as in integration tests (right?), you can remove your business logic
|
460
|
-
from your controller tests.
|
443
|
+
One of the advantages of using interaktors is how much they simplify controllers and their tests. Because you're testing your interaktors thoroughly in isolation as well as in integration tests (right?), you can remove your business logic from your controller tests.
|
461
444
|
|
462
445
|
```ruby
|
463
446
|
class SessionsController < ApplicationController
|
@@ -485,12 +468,12 @@ end
|
|
485
468
|
describe SessionsController do
|
486
469
|
describe "#create" do
|
487
470
|
before do
|
488
|
-
expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(
|
471
|
+
expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(result)
|
489
472
|
end
|
490
473
|
|
491
474
|
context "when successful" do
|
492
475
|
let(:user) { double(:user, id: 1) }
|
493
|
-
let(:
|
476
|
+
let(:result) { double(:result, success?: true, user: user, token: "token") }
|
494
477
|
|
495
478
|
it "saves the user's secret token in the session" do
|
496
479
|
expect {
|
@@ -508,7 +491,7 @@ describe SessionsController do
|
|
508
491
|
end
|
509
492
|
|
510
493
|
context "when unsuccessful" do
|
511
|
-
let(:
|
494
|
+
let(:result) { double(:result, success?: false, message: "message") }
|
512
495
|
|
513
496
|
it "sets a flash message" do
|
514
497
|
expect {
|
@@ -528,36 +511,8 @@ describe SessionsController do
|
|
528
511
|
end
|
529
512
|
```
|
530
513
|
|
531
|
-
This controller test will have to change very little during the life of the
|
532
|
-
application because all of the magic happens in the interaktor.
|
514
|
+
This controller test will have to change very little during the life of the application because all of the magic happens in the interaktor.
|
533
515
|
|
534
516
|
### Rails
|
535
517
|
|
536
|
-
[
|
537
|
-
put our interaktors in `app/interaktors` and we name them as verbs:
|
538
|
-
|
539
|
-
- `AddProductToCart`
|
540
|
-
- `AuthenticateUser`
|
541
|
-
- `PlaceOrder`
|
542
|
-
- `RegisterUser`
|
543
|
-
- `RemoveProductFromCart`
|
544
|
-
|
545
|
-
See: [Interaktor Rails](https://github.com/collectiveidea/interaktor-rails)
|
546
|
-
|
547
|
-
## Contributions
|
548
|
-
|
549
|
-
Interaktor is open source and contributions from the community are encouraged!
|
550
|
-
No contribution is too small.
|
551
|
-
|
552
|
-
See Interaktor's
|
553
|
-
[contribution guidelines](CONTRIBUTING.md) for more information.
|
554
|
-
|
555
|
-
## Thank You
|
556
|
-
|
557
|
-
A very special thank you to [Attila Domokos](https://github.com/adomokos) for
|
558
|
-
his fantastic work on [LightService](https://github.com/adomokos/light-service).
|
559
|
-
Interaktor is inspired heavily by the concepts put to code by Attila.
|
560
|
-
|
561
|
-
Interaktor was born from a desire for a slightly simplified interface. We
|
562
|
-
understand that this is a matter of personal preference, so please take a look
|
563
|
-
at LightService as well!
|
518
|
+
Interactor provided [interactor-rails](https://github.com/collectiveidea/interactor-rails), which ensures `app/interactors` is included in your autoload paths, and provides generators for new interactors. I have no intention of maintaining generators but if someone feels strongly enough to submit a pull request to include the functionality in _this_ gem (not a separate Rails one) then I will be happy to take a look. Making sure `app/interactors` is included in your autoload paths is something I would like to do soon.
|