disposable 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -5
  3. data/CHANGES.md +4 -0
  4. data/Gemfile +1 -1
  5. data/README.md +154 -1
  6. data/database.sqlite3 +0 -0
  7. data/disposable.gemspec +7 -7
  8. data/gemfiles/Gemfile.rails-3.0.lock +10 -8
  9. data/gemfiles/Gemfile.rails-3.2.lock +9 -7
  10. data/gemfiles/Gemfile.rails-4.0.lock +9 -7
  11. data/gemfiles/Gemfile.rails-4.1.lock +10 -8
  12. data/lib/disposable.rb +6 -7
  13. data/lib/disposable/callback.rb +174 -0
  14. data/lib/disposable/composition.rb +21 -58
  15. data/lib/disposable/expose.rb +49 -0
  16. data/lib/disposable/twin.rb +85 -38
  17. data/lib/disposable/twin/builder.rb +12 -30
  18. data/lib/disposable/twin/changed.rb +50 -0
  19. data/lib/disposable/twin/collection.rb +95 -0
  20. data/lib/disposable/twin/composition.rb +43 -15
  21. data/lib/disposable/twin/option.rb +1 -1
  22. data/lib/disposable/twin/persisted.rb +20 -0
  23. data/lib/disposable/twin/property_processor.rb +29 -0
  24. data/lib/disposable/twin/representer.rb +42 -14
  25. data/lib/disposable/twin/save.rb +19 -34
  26. data/lib/disposable/twin/schema.rb +31 -0
  27. data/lib/disposable/twin/setup.rb +38 -0
  28. data/lib/disposable/twin/sync.rb +114 -0
  29. data/lib/disposable/version.rb +1 -1
  30. data/test/api_semantics_test.rb +263 -0
  31. data/test/callback_group_test.rb +222 -0
  32. data/test/callbacks_test.rb +450 -0
  33. data/test/example.rb +40 -0
  34. data/test/expose_test.rb +92 -0
  35. data/test/persisted_test.rb +101 -0
  36. data/test/test_helper.rb +64 -0
  37. data/test/twin/benchmarking.rb +33 -0
  38. data/test/twin/builder_test.rb +32 -0
  39. data/test/twin/changed_test.rb +108 -0
  40. data/test/twin/collection_test.rb +223 -0
  41. data/test/twin/composition_test.rb +56 -25
  42. data/test/twin/expose_test.rb +73 -0
  43. data/test/twin/feature_test.rb +61 -0
  44. data/test/twin/from_test.rb +37 -0
  45. data/test/twin/inherit_test.rb +57 -0
  46. data/test/twin/option_test.rb +27 -0
  47. data/test/twin/readable_test.rb +57 -0
  48. data/test/twin/save_test.rb +192 -0
  49. data/test/twin/schema_test.rb +69 -0
  50. data/test/twin/setup_test.rb +139 -0
  51. data/test/twin/skip_unchanged_test.rb +64 -0
  52. data/test/twin/struct_test.rb +168 -0
  53. data/test/twin/sync_option_test.rb +228 -0
  54. data/test/twin/sync_test.rb +128 -0
  55. data/test/twin/twin_test.rb +49 -128
  56. data/test/twin/writeable_test.rb +56 -0
  57. metadata +106 -20
  58. data/STUFF +0 -4
  59. data/lib/disposable/twin/finders.rb +0 -29
  60. data/lib/disposable/twin/new.rb +0 -30
  61. data/lib/disposable/twin/save_.rb +0 -21
  62. data/test/composition_test.rb +0 -102
@@ -0,0 +1,37 @@
1
+ require 'test_helper'
2
+
3
+ class FromTest < MiniTest::Spec
4
+ module Model
5
+ Album = Struct.new(:name, :composer)
6
+ Artist = Struct.new(:realname)
7
+ end
8
+
9
+
10
+ module Twin
11
+ class Album < Disposable::Twin
12
+ feature Sync
13
+ feature Save
14
+ feature Disposable::Twin::Expose
15
+
16
+ property :full_name, from: :name
17
+
18
+ property :artist, from: :composer do
19
+ property :name, from: :realname
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+ let (:composer) { Model::Artist.new("AFI").extend(Disposable::Saveable) }
26
+ let (:album) { Model::Album.new("Black Sails In The Sunset", composer).extend(Disposable::Saveable) }
27
+ let (:twin) { Twin::Album.new(album) }
28
+
29
+ it do
30
+ twin.full_name.must_equal "Black Sails In The Sunset"
31
+ twin.artist.name.must_equal "AFI"
32
+
33
+ twin.save
34
+
35
+
36
+ end
37
+ end
@@ -0,0 +1,57 @@
1
+ require "test_helper"
2
+
3
+ class InheritTest < MiniTest::Spec
4
+ module Model
5
+ Song = Struct.new(:title, :album)
6
+ Album = Struct.new(:name, :songs, :artist)
7
+ Artist = Struct.new(:name)
8
+ end
9
+
10
+ module Twin
11
+ class Album < Disposable::Twin
12
+ feature Setup
13
+
14
+ property :name, fromage: :_name
15
+
16
+ collection :songs do
17
+ property :name
18
+ end
19
+
20
+ property :artist do
21
+ property :name
22
+
23
+ def artist_id
24
+ 1
25
+ end
26
+ end
27
+ end
28
+
29
+ class EmptyCompilation < Album
30
+ end
31
+
32
+ class Compilation < Album
33
+ property :name, writeable: false, inherit: true
34
+
35
+ property :artist, inherit: true do
36
+
37
+ end
38
+ end
39
+ end
40
+
41
+ # definitions are not shared.
42
+ it do
43
+ Twin::Album.representer_class.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:fromage=>:_name, :private_name=>:name, :pass_options=>true, :parse_filter=>[], :render_filter=>[], :as=>\"name\"}>"
44
+ Twin::Compilation.representer_class.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:fromage=>:_name, :private_name=>:name, :pass_options=>true, :parse_filter=>[], :render_filter=>[], :as=>\"name\", :writeable=>false, :inherit=>true}>"
45
+ end
46
+
47
+
48
+ let (:album) { Model::Album.new("In The Meantime And Inbetween Time", [], Model::Artist.new) }
49
+
50
+ it { Twin::Album.new(album).artist.artist_id.must_equal 1 }
51
+
52
+ # inherit inline twins when not overriding.
53
+ it { Twin::EmptyCompilation.new(album).artist.artist_id.must_equal 1 }
54
+
55
+ # inherit inline twins when overriding.
56
+ it { Twin::Compilation.new(album).artist.artist_id.must_equal 1 }
57
+ end
@@ -0,0 +1,27 @@
1
+ # require "test_helper"
2
+
3
+ # class TwinOptionTest < TwinTest
4
+ # class Song < Disposable::Twin
5
+ # property :id # DISCUSS: needed for #save.
6
+ # property :title
7
+
8
+ # option :preview?
9
+ # option :highlight?
10
+ # end
11
+
12
+ # let (:song) { Model::Song.new(1, "Broken") }
13
+ # let (:twin) { Song.new(song, :preview? => false) }
14
+
15
+
16
+ # # properties are read from model.
17
+ # it { twin.id.must_equal 1 }
18
+ # it { twin.title.must_equal "Broken" }
19
+
20
+ # # option is not delegated to model.
21
+ # it { twin.preview?.must_equal false }
22
+ # # not passing option means zero.
23
+ # it { twin.highlight?.must_equal nil }
24
+
25
+ # # passing both options.
26
+ # it { Song.new(song, preview?: true, highlight?: false).preview?.must_equal true }
27
+ # end
@@ -0,0 +1,57 @@
1
+ require 'test_helper'
2
+
3
+ class ReadableTest < MiniTest::Spec
4
+ Credentials = Struct.new(:password, :credit_card) do
5
+ def password
6
+ raise "don't call me!"
7
+ end
8
+ end
9
+
10
+ CreditCard = Struct.new(:name, :number) do
11
+ def number
12
+ raise "don't call me!"
13
+ end
14
+ end
15
+
16
+ class PasswordForm < Disposable::Twin
17
+ feature Setup
18
+ feature Sync
19
+
20
+ property :password, readable: false
21
+
22
+ property :credit_card do
23
+ property :name
24
+ property :number, readable: false
25
+ end
26
+ end
27
+
28
+ let (:cred) { Credentials.new("secret", CreditCard.new("Jonny", "0987654321")) }
29
+
30
+ let (:twin) { PasswordForm.new(cred) }
31
+
32
+ it {
33
+ twin.password.must_equal nil # not readable.
34
+ twin.credit_card.name.must_equal "Jonny"
35
+ twin.credit_card.number.must_equal nil # not readable.
36
+
37
+ # manual setting on the twin works.
38
+ twin.password = "123"
39
+ twin.password.must_equal "123"
40
+
41
+ twin.credit_card.number = "456"
42
+ twin.credit_card.number.must_equal "456"
43
+
44
+ twin.sync
45
+
46
+ # it writes, but does not read.
47
+ cred.inspect.must_equal '#<struct ReadableTest::Credentials password="123", credit_card=#<struct ReadableTest::CreditCard name="Jonny", number="456">>'
48
+
49
+ # test sync{}.
50
+ hash = {}
51
+ twin.sync do |nested|
52
+ hash = nested
53
+ end
54
+
55
+ hash.must_equal("password"=> "123", "credit_card"=>{"name"=>"Jonny", "number"=>"456"})
56
+ }
57
+ end
@@ -0,0 +1,192 @@
1
+ require 'test_helper'
2
+
3
+ class SaveTest < MiniTest::Spec
4
+ module Model
5
+ Song = Struct.new(:title, :composer)
6
+ Album = Struct.new(:name, :songs, :artist)
7
+ Artist = Struct.new(:name)
8
+ end
9
+
10
+
11
+ module Twin
12
+ class Album < Disposable::Twin
13
+ feature Setup
14
+ feature Sync
15
+ feature Save
16
+
17
+ property :name
18
+
19
+ collection :songs do
20
+ property :title
21
+
22
+ property :composer do
23
+ property :name
24
+ end
25
+ end
26
+
27
+ property :artist do
28
+ property :name
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+ let (:song) { Model::Song.new().extend(Disposable::Saveable) }
35
+ let (:composer) { Model::Artist.new(nil).extend(Disposable::Saveable) }
36
+ let (:song_with_composer) { Model::Song.new(nil, composer).extend(Disposable::Saveable) }
37
+ let (:artist) { Model::Artist.new(nil).extend(Disposable::Saveable) }
38
+
39
+
40
+ let (:album) { Model::Album.new(nil, [song, song_with_composer], artist).extend(Disposable::Saveable) }
41
+
42
+ let (:twin) { Twin::Album.new(album) }
43
+
44
+ # with populated model.
45
+ it do
46
+ fill_out!(twin)
47
+
48
+ twin.save
49
+
50
+ # sync happened.
51
+ album.name.must_equal "Live And Dangerous"
52
+ album.songs[0].must_be_instance_of Model::Song
53
+ album.songs[1].must_be_instance_of Model::Song
54
+ album.songs[0].title.must_equal "Southbound"
55
+ album.songs[1].title.must_equal "The Boys Are Back In Town"
56
+ album.songs[1].composer.must_be_instance_of Model::Artist
57
+ album.songs[1].composer.name.must_equal "Lynott"
58
+ album.artist.must_be_instance_of Model::Artist
59
+ album.artist.name.must_equal "Thin Lizzy"
60
+
61
+ # saved?
62
+ album.saved?.must_equal true
63
+ album.songs[0].saved?.must_equal true
64
+ album.songs[1].saved?.must_equal true
65
+ album.songs[1].composer.saved?.must_equal true
66
+ album.artist.saved?.must_equal true
67
+ end
68
+
69
+ #save returns result.
70
+ it { twin.save.must_equal true }
71
+ it do
72
+ album.instance_eval { def save; false; end }
73
+ twin.save.must_equal false
74
+ end
75
+
76
+ # with save{}.
77
+ it do
78
+ twin = Twin::Album.new(album)
79
+
80
+ # this usually happens in Contract::Validate or in from_* in a representer
81
+ fill_out!(twin)
82
+
83
+ nested_hash = nil
84
+ twin.save do |hash|
85
+ nested_hash = hash
86
+ end
87
+
88
+ nested_hash.must_equal({"name"=>"Live And Dangerous", "songs"=>[{"title"=>"Southbound"}, {"title"=>"The Boys Are Back In Town", "composer"=>{"name"=>"Lynott"}}], "artist"=>{"name"=>"Thin Lizzy"}})
89
+
90
+ # nothing written to model.
91
+ album.name.must_equal nil
92
+ album.songs[0].title.must_equal nil
93
+ album.songs[1].title.must_equal nil
94
+ album.songs[1].composer.name.must_equal nil
95
+ album.artist.name.must_equal nil
96
+
97
+ # nothing saved.
98
+ # saved?
99
+ album.saved?.must_equal nil
100
+ album.songs[0].saved?.must_equal nil
101
+ album.songs[1].saved?.must_equal nil
102
+ album.songs[1].composer.saved?.must_equal nil
103
+ album.artist.saved?.must_equal nil
104
+ end
105
+
106
+
107
+ # save: false
108
+ module Twin
109
+ class AlbumWithSaveFalse < Disposable::Twin
110
+ feature Setup
111
+ feature Sync
112
+ feature Save
113
+
114
+ property :name
115
+
116
+ collection :songs, save: false do
117
+ property :title
118
+
119
+ property :composer do
120
+ property :name
121
+ end
122
+ end
123
+
124
+ property :artist do
125
+ property :name
126
+ end
127
+ end
128
+ end
129
+
130
+ # with save: false.
131
+ it do
132
+ twin = Twin::AlbumWithSaveFalse.new(album)
133
+
134
+ fill_out!(twin)
135
+
136
+ twin.save
137
+
138
+ # sync happened.
139
+ album.name.must_equal "Live And Dangerous"
140
+ album.songs[0].must_be_instance_of Model::Song
141
+ album.songs[1].must_be_instance_of Model::Song
142
+ album.songs[0].title.must_equal "Southbound"
143
+ album.songs[1].title.must_equal "The Boys Are Back In Town"
144
+ album.songs[1].composer.must_be_instance_of Model::Artist
145
+ album.songs[1].composer.name.must_equal "Lynott"
146
+ album.artist.must_be_instance_of Model::Artist
147
+ album.artist.name.must_equal "Thin Lizzy"
148
+
149
+ # saved?
150
+ album.saved?.must_equal true
151
+ album.songs[0].saved?.must_equal nil
152
+ album.songs[1].saved?.must_equal nil
153
+ album.songs[1].composer.saved?.must_equal nil # doesn't get saved.
154
+ album.artist.saved?.must_equal true
155
+ end
156
+
157
+ def fill_out!(twin)
158
+ twin.name = "Live And Dangerous"
159
+ twin.songs[0].title = "Southbound"
160
+ twin.songs[1].title = "The Boys Are Back In Town"
161
+ twin.songs[1].composer.name = "Lynott"
162
+ twin.artist.name = "Thin Lizzy"
163
+ end
164
+ end
165
+
166
+
167
+ # TODO: with block
168
+
169
+ # class SaveWithDynamicOptionsTest < MiniTest::Spec
170
+ # Song = Struct.new(:id, :title, :length) do
171
+ # include Disposable::Saveable
172
+ # end
173
+
174
+ # class SongForm < Reform::Form
175
+ # property :title#, save: false
176
+ # property :length, virtual: true
177
+ # end
178
+
179
+ # let (:song) { Song.new }
180
+ # let (:form) { SongForm.new(song) }
181
+
182
+ # # we have access to original input value and outside parameters.
183
+ # it "xxx" do
184
+ # form.validate("title" => "A Poor Man's Memory", "length" => 10)
185
+ # length_seconds = 120
186
+ # form.save(length: lambda { |value, options| form.model.id = "#{value}: #{length_seconds}" })
187
+
188
+ # song.title.must_equal "A Poor Man's Memory"
189
+ # song.length.must_equal nil
190
+ # song.id.must_equal "10: 120"
191
+ # end
192
+ # end
@@ -0,0 +1,69 @@
1
+ require "test_helper"
2
+
3
+ require "disposable/twin/schema"
4
+
5
+ class SchemaTest < MiniTest::Spec
6
+ module Representer
7
+ include Representable
8
+
9
+ property :id
10
+ property :title, writeable: false, deserializer: {skip_parse: "skip lambda"}
11
+ property :songs, readable: false, deserializer: {skip_parse: "another lambda", music: true, writeable: false} do
12
+ property :name, as: "Name", deserializer: {skip_parse: "a crazy cool instance method"}
13
+ end
14
+ end
15
+
16
+ module Hello
17
+ def hello
18
+ "hello"
19
+ end
20
+ end
21
+
22
+ module Ciao
23
+ def ciao
24
+ "ciao"
25
+ end
26
+ end
27
+
28
+ module Gday
29
+ def hello
30
+ "G'day"
31
+ end
32
+ end
33
+
34
+ it do
35
+ decorator = Disposable::Twin::Schema.from(Representer, superclass: Representable::Decorator,
36
+ include: [Hello, Gday, Ciao], # Hello will win over Gday.
37
+ options_from: :deserializer,
38
+ representer_from: lambda { |nested| nested }
39
+ )
40
+
41
+ # include: works.
42
+ decorator.new(nil).hello.must_equal "hello"
43
+ decorator.new(nil).ciao.must_equal "ciao"
44
+
45
+ decorator.representable_attrs.get(:id).inspect.must_equal "#<Representable::Definition ==>id @options={:parse_filter=>[], :render_filter=>[], :as=>\"id\"}>"
46
+ decorator.representable_attrs.get(:title).inspect.must_equal "#<Representable::Definition ==>title @options={:writeable=>false, :deserializer=>{:skip_parse=>\"skip lambda\"}, :parse_filter=>[], :render_filter=>[], :as=>\"title\", :skip_parse=>\"skip lambda\"}>"
47
+
48
+ songs = decorator.representable_attrs.get(:songs)
49
+ options = songs.instance_variable_get(:@options)
50
+ nested_extend = options.delete(:extend)
51
+ options.inspect.must_equal "{:readable=>false, :deserializer=>{:skip_parse=>\"another lambda\", :music=>true, :writeable=>false}, :parse_filter=>[], :render_filter=>[], :as=>\"songs\", :_inline=>true, :skip_parse=>\"another lambda\", :music=>true, :writeable=>false}"
52
+
53
+ # nested works.
54
+ nested_extend.new(nil).hello.must_equal "hello"
55
+ nested_extend.new(nil).ciao.must_equal "ciao"
56
+
57
+ nested_extend.representable_attrs.get(:name).inspect.must_equal "#<Representable::Definition ==>name @options={:as=>\"Name\", :deserializer=>{:skip_parse=>\"a crazy cool instance method\"}, :parse_filter=>[], :render_filter=>[], :skip_parse=>\"a crazy cool instance method\"}>"
58
+ end
59
+
60
+ # :options_from and :include is optional
61
+ it do
62
+ decorator = Disposable::Twin::Schema.from(Representer, superclass: Representable::Decorator,
63
+ representer_from: lambda { |nested| nested }
64
+ )
65
+
66
+ decorator.representable_attrs.get(:id).inspect.must_equal "#<Representable::Definition ==>id @options={:parse_filter=>[], :render_filter=>[], :as=>\"id\"}>"
67
+ decorator.representable_attrs.get(:title).inspect.must_equal "#<Representable::Definition ==>title @options={:writeable=>false, :deserializer=>{:skip_parse=>\"skip lambda\"}, :parse_filter=>[], :render_filter=>[], :as=>\"title\"}>"
68
+ end
69
+ end
@@ -0,0 +1,139 @@
1
+ require "test_helper"
2
+
3
+ class TwinSetupTest < MiniTest::Spec
4
+ module Model
5
+ Song = Struct.new(:id, :title, :album, :composer)
6
+ Album = Struct.new(:id, :name, :songs, :artist)
7
+ Artist = Struct.new(:id)
8
+ end
9
+
10
+
11
+ module Twin
12
+ class Album < Disposable::Twin
13
+ property :id
14
+ property :name
15
+ collection :songs, twin: lambda { |*| Song }
16
+ property :artist, twin: lambda { |*| Artist }
17
+
18
+ include Setup
19
+ end
20
+
21
+ class Song < Disposable::Twin
22
+ property :id
23
+ property :composer, twin: lambda { |*| Artist }
24
+
25
+ include Setup
26
+ end
27
+
28
+ class Artist < Disposable::Twin
29
+ property :id
30
+
31
+ include Setup
32
+ end
33
+ end
34
+
35
+
36
+ let (:song) { Model::Song.new(1, "Broken", nil) }
37
+ let (:composer) { Model::Artist.new(2) }
38
+ let (:song_with_composer) { Model::Song.new(1, "Broken", nil, composer) }
39
+ let (:artist) { Model::Artist.new(9) }
40
+
41
+ describe "with songs: [song, song{composer}]" do
42
+ let (:album) { Model::Album.new(1, "The Rest Is Silence", [song, song_with_composer], artist) }
43
+
44
+ it do
45
+ twin = Twin::Album.new(album)
46
+
47
+ twin.songs.size.must_equal 2
48
+ twin.songs.must_be_instance_of Disposable::Twin::Collection
49
+
50
+ twin.songs[0].must_be_instance_of Twin::Song
51
+ twin.songs[0].id.must_equal 1
52
+
53
+ twin.songs[1].must_be_instance_of Twin::Song
54
+ twin.songs[1].id.must_equal 1
55
+ twin.songs[1].composer.must_be_instance_of Twin::Artist
56
+ twin.songs[1].composer.id.must_equal 2
57
+ end
58
+ end
59
+
60
+ describe "with songs: [] and artist: nil" do
61
+ let (:album) { Model::Album.new(1, "The Rest Is Silence", [], nil) }
62
+
63
+ it do
64
+ twin = Twin::Album.new(album)
65
+
66
+ twin.songs.size.must_equal 0
67
+ twin.songs.must_be_instance_of Disposable::Twin::Collection
68
+ end
69
+ end
70
+
71
+ # DISCUSS: do we need to cover that (songs: nil in model)?
72
+ # describe "with non-existent :songs" do
73
+ # let (:album) { Model::Album.new(1, "The Rest Is Silence", nil) }
74
+
75
+ # it do
76
+ # twin = Twin::Album.new(album)
77
+
78
+ # twin.songs.size.must_equal 0
79
+ # twin.songs.must_be_instance_of Disposable::Twin::Collection
80
+ # end
81
+ # end
82
+ end
83
+
84
+ # test inline twin building and setup.
85
+ class TwinSetupWithInlineTwinsTest < MiniTest::Spec
86
+ module Model
87
+ Song = Struct.new(:id, :composer)
88
+ Album = Struct.new(:id, :name, :songs, :artist)
89
+ Artist = Struct.new(:id)
90
+ end
91
+
92
+ class AlbumForm < Disposable::Twin
93
+ feature Setup
94
+
95
+ property :id
96
+ property :name
97
+
98
+ collection :songs do # default_inline_class: Disposable::Twin
99
+ property :id
100
+
101
+ property :composer do
102
+ property :id
103
+ end
104
+ end
105
+
106
+ property :artist do
107
+ property :id
108
+ end
109
+ end
110
+
111
+ let (:song) { Model::Song.new(1) }
112
+ let (:composer) { Model::Artist.new(2) }
113
+ let (:song_with_composer) { Model::Song.new(3, composer) }
114
+ let (:artist) { Model::Artist.new(9) }
115
+ let (:album) { Model::Album.new(0, "Toto Live", [song, song_with_composer], artist) }
116
+
117
+ it do
118
+ twin = AlbumForm.new(album)
119
+ # pp twin
120
+
121
+ twin.id.must_equal 0
122
+ twin.name.must_equal "Toto Live"
123
+
124
+ twin.artist.must_be_kind_of Disposable::Twin
125
+ twin.artist.id.must_equal 9
126
+
127
+ twin.songs.must_be_instance_of Disposable::Twin::Collection
128
+
129
+ # nil nested objects work (no composer)
130
+ twin.songs[0].must_be_kind_of Disposable::Twin
131
+ twin.songs[0].id.must_equal 1
132
+
133
+ twin.songs[1].must_be_kind_of Disposable::Twin
134
+ twin.songs[1].id.must_equal 3
135
+
136
+ twin.songs[1].composer.must_be_kind_of Disposable::Twin
137
+ twin.songs[1].composer.id.must_equal 2
138
+ end
139
+ end