smartest 0.1.0.alpha1

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.
@@ -0,0 +1,1137 @@
1
+ # Smartest Design
2
+
3
+ This document records the design of Smartest.
4
+
5
+ Smartest is a Ruby test runner inspired by pytest, Vitest, and Playwright Test, but with an API that should feel natural in Ruby.
6
+
7
+ ## Design summary
8
+
9
+ Smartest provides:
10
+
11
+ ```ruby
12
+ test("factorial") do
13
+ expect(1 * 2 * 3).to eq(6)
14
+ end
15
+ ```
16
+
17
+ Fixture usage:
18
+
19
+ ```ruby
20
+ test("GET /me") do |logged_in_client:|
21
+ response = logged_in_client.get("/me")
22
+
23
+ expect(response.status).to eq(200)
24
+ end
25
+ ```
26
+
27
+ Fixture definitions:
28
+
29
+ ```ruby
30
+ class WebFixture < Smartest::Fixture
31
+ fixture :server do
32
+ server = TestServer.start
33
+ cleanup { server.stop }
34
+
35
+ server.wait_until_ready!
36
+ server
37
+ end
38
+
39
+ fixture :client do |server:|
40
+ Client.new(base_url: server.url)
41
+ end
42
+
43
+ fixture :user do
44
+ User.create!(email: "alice@example.com")
45
+ end
46
+
47
+ fixture :logged_in_client do |client:, user:|
48
+ client.login(user)
49
+ client
50
+ end
51
+ end
52
+
53
+ use_fixture WebFixture
54
+ ```
55
+
56
+ The core decision is:
57
+
58
+ > Fixture dependencies and test fixture usage are expressed as required keyword arguments.
59
+
60
+ ## Why keyword arguments?
61
+
62
+ Several forms were considered.
63
+
64
+ ### Positional test fixture injection
65
+
66
+ ```ruby
67
+ test("GET /me") do |logged_in_client|
68
+ end
69
+ ```
70
+
71
+ This is concise and close to pytest.
72
+
73
+ However, in Ruby it reads like an ordinary block argument. It is not obvious that the value is injected by name.
74
+
75
+ It also creates ambiguity:
76
+
77
+ ```ruby
78
+ test("example") do |user, article|
79
+ end
80
+ ```
81
+
82
+ Are `user` and `article` matched by position or by name?
83
+
84
+ Smartest avoids this ambiguity.
85
+
86
+ ### Keyword test fixture injection
87
+
88
+ ```ruby
89
+ test("GET /me") do |logged_in_client:|
90
+ end
91
+ ```
92
+
93
+ This reads as named input to the test.
94
+
95
+ Ruby exposes the name clearly through `Proc#parameters`:
96
+
97
+ ```ruby
98
+ proc { |logged_in_client:| }.parameters
99
+ # => [[:keyreq, :logged_in_client]]
100
+ ```
101
+
102
+ This gives Smartest a stable way to discover requested fixtures.
103
+
104
+ ### `with:` fixture dependencies
105
+
106
+ Considered:
107
+
108
+ ```ruby
109
+ fixture :client, with: [:server] do |server|
110
+ Client.new(base_url: server.url)
111
+ end
112
+ ```
113
+
114
+ This makes dependency declaration explicit, but it duplicates information.
115
+
116
+ The dependency appears in two places:
117
+
118
+ ```ruby
119
+ with: [:server]
120
+ do |server|
121
+ ```
122
+
123
+ This can drift:
124
+
125
+ ```ruby
126
+ fixture :client, with: [:user, :server] do |server, user|
127
+ end
128
+ ```
129
+
130
+ Keyword arguments avoid this:
131
+
132
+ ```ruby
133
+ fixture :client do |server:, user:|
134
+ end
135
+ ```
136
+
137
+ The names are the API. The order does not matter.
138
+
139
+ ### Implicit method-call fixture dependencies
140
+
141
+ Considered:
142
+
143
+ ```ruby
144
+ fixture :client do
145
+ Client.new(base_url: server.url)
146
+ end
147
+ ```
148
+
149
+ This is very Ruby-like, but dependency discovery requires executing code.
150
+
151
+ It makes static dependency analysis difficult.
152
+
153
+ It also makes circular dependency detection later and less clear.
154
+
155
+ Smartest prefers:
156
+
157
+ ```ruby
158
+ fixture :client do |server:|
159
+ Client.new(base_url: server.url)
160
+ end
161
+ ```
162
+
163
+ Dependencies are explicit and discoverable before fixture execution.
164
+
165
+ ## Why not `resource` for setup/teardown?
166
+
167
+ Playwright Test-style fixtures often use a `use` callback:
168
+
169
+ ```ruby
170
+ fixture :server do |use|
171
+ server = TestServer.start
172
+
173
+ use.call(server)
174
+ ensure
175
+ server&.stop
176
+ end
177
+ ```
178
+
179
+ This is powerful because the fixture surrounds the test body.
180
+
181
+ However, it complicates the execution model.
182
+
183
+ To support this fully, Smartest would need to build an around-chain:
184
+
185
+ ```text
186
+ server setup
187
+ temp_dir setup
188
+ test body
189
+ temp_dir teardown
190
+ server teardown
191
+ ```
192
+
193
+ This is especially complex when fixtures depend on other fixtures.
194
+
195
+ Smartest instead chooses `cleanup` for the MVP:
196
+
197
+ ```ruby
198
+ fixture :server do
199
+ server = TestServer.start
200
+ cleanup { server.stop }
201
+
202
+ server.wait_until_ready!
203
+ server
204
+ end
205
+ ```
206
+
207
+ This has several advantages:
208
+
209
+ - fixtures always return values
210
+ - teardown is optional
211
+ - teardown is local to the fixture that owns the resource
212
+ - implementation is simple
213
+ - fixture dependencies remain ordinary recursive resolution
214
+ - cleanup runs in `ensure`
215
+
216
+ Not every fixture needs teardown, so teardown should not shape the entire fixture API.
217
+
218
+ ## Fixture model
219
+
220
+ A fixture is a named value provider.
221
+
222
+ ```ruby
223
+ fixture :user do
224
+ User.create!(name: "Alice")
225
+ end
226
+ ```
227
+
228
+ A fixture may depend on other fixtures.
229
+
230
+ ```ruby
231
+ fixture :article do |user:|
232
+ Article.create!(author: user)
233
+ end
234
+ ```
235
+
236
+ A fixture may register cleanup.
237
+
238
+ ```ruby
239
+ fixture :temp_dir do
240
+ dir = Dir.mktmpdir
241
+ cleanup { FileUtils.rm_rf(dir) }
242
+ dir
243
+ end
244
+ ```
245
+
246
+ By default, a fixture value is cached per test.
247
+
248
+ Within one test, resolving the same test-scoped fixture multiple times returns
249
+ the same value.
250
+
251
+ Across tests, test-scoped fixtures are re-created. `suite_fixture` values are
252
+ cached for the suite and shared intentionally.
253
+
254
+ ## Test model
255
+
256
+ A test is a named block.
257
+
258
+ ```ruby
259
+ test("name") do
260
+ end
261
+ ```
262
+
263
+ A test may request fixtures through required keyword arguments.
264
+
265
+ ```ruby
266
+ test("name") do |user:, article:|
267
+ end
268
+ ```
269
+
270
+ Smartest resolves these names and calls:
271
+
272
+ ```ruby
273
+ context.instance_exec(**kwargs, &test_case.block)
274
+ ```
275
+
276
+ The test body runs with `self` set to an `ExecutionContext`.
277
+
278
+ ## Execution context
279
+
280
+ The execution context is the object used as `self` for test bodies.
281
+
282
+ Responsibilities:
283
+
284
+ - provide `expect`
285
+ - provide matchers such as `eq`
286
+ - provide test helper methods
287
+ - avoid polluting global objects
288
+
289
+ Tests are run as:
290
+
291
+ ```ruby
292
+ context.instance_exec(**fixtures, &block)
293
+ ```
294
+
295
+ This keeps the top-level DSL small.
296
+
297
+ Only `test`, `fixture`, and `use_fixture` need to be globally available when using `smartest/autorun`.
298
+
299
+ ## Core architecture
300
+
301
+ ```text
302
+ Smartest
303
+ └── Suite
304
+ ├── TestRegistry
305
+ └── FixtureClassRegistry
306
+
307
+ Runner
308
+ ├── loads TestCase objects
309
+ ├── creates ExecutionContext
310
+ ├── creates FixtureSet
311
+ ├── resolves keyword fixtures
312
+ ├── executes test body
313
+ ├── runs cleanup
314
+ └── reports TestResult
315
+ ```
316
+
317
+ ## Runtime flow
318
+
319
+ Given:
320
+
321
+ ```ruby
322
+ test("GET /me") do |logged_in_client:|
323
+ end
324
+ ```
325
+
326
+ and fixtures:
327
+
328
+ ```ruby
329
+ fixture :logged_in_client do |client:, user:|
330
+ client.login(user)
331
+ client
332
+ end
333
+
334
+ fixture :client do |server:|
335
+ Client.new(base_url: server.url)
336
+ end
337
+
338
+ fixture :server do
339
+ server = TestServer.start
340
+ cleanup { server.stop }
341
+ server
342
+ end
343
+
344
+ fixture :user do
345
+ User.create!(email: "alice@example.com")
346
+ end
347
+ ```
348
+
349
+ Resolution:
350
+
351
+ ```text
352
+ test requires logged_in_client
353
+
354
+ resolve logged_in_client
355
+ requires client
356
+ resolve client
357
+ requires server
358
+ resolve server
359
+ evaluate server block
360
+ register cleanup
361
+ cache server
362
+ evaluate client block with server:
363
+ cache client
364
+ requires user
365
+ resolve user
366
+ evaluate user block
367
+ cache user
368
+ evaluate logged_in_client block with client:, user:
369
+ cache logged_in_client
370
+
371
+ execute test body with logged_in_client:
372
+
373
+ run cleanup stack in reverse order
374
+ ```
375
+
376
+ ## Fixture caching
377
+
378
+ `FixtureSet` owns a cache for one fixture scope.
379
+
380
+ ```ruby
381
+ @cache = {
382
+ server: server_object,
383
+ client: client_object,
384
+ user: user_object
385
+ }
386
+ ```
387
+
388
+ The test-scoped cache is created fresh for each test. The suite-scoped cache is
389
+ shared for the runner lifetime.
390
+
391
+ This keeps regular fixtures isolated while allowing explicit suite fixtures for
392
+ expensive shared resources.
393
+
394
+ ## Cleanup stack
395
+
396
+ `FixtureSet` owns a cleanup stack for one fixture scope.
397
+
398
+ ```ruby
399
+ @cleanups = []
400
+ ```
401
+
402
+ Fixture blocks can call:
403
+
404
+ ```ruby
405
+ cleanup { resource.close }
406
+ ```
407
+
408
+ This delegates to:
409
+
410
+ ```ruby
411
+ fixture_set.add_cleanup(&block)
412
+ ```
413
+
414
+ For test-scoped fixtures, cleanup runs after the test in reverse order:
415
+
416
+ ```ruby
417
+ @cleanups.reverse_each(&:call)
418
+ ```
419
+
420
+ For suite-scoped fixtures, cleanup runs after all tests. Reverse order matters
421
+ because later resources may depend on earlier ones.
422
+
423
+ Example:
424
+
425
+ ```ruby
426
+ fixture :server do
427
+ server = TestServer.start
428
+ cleanup { server.stop }
429
+ server
430
+ end
431
+
432
+ fixture :browser do |server:|
433
+ browser = Browser.launch(server.url)
434
+ cleanup { browser.close }
435
+ browser
436
+ end
437
+ ```
438
+
439
+ Cleanup should run:
440
+
441
+ ```text
442
+ browser.close
443
+ server.stop
444
+ ```
445
+
446
+ ## Dependency extraction
447
+
448
+ Smartest uses `Proc#parameters`.
449
+
450
+ Required keyword arguments:
451
+
452
+ ```ruby
453
+ proc { |server:| }.parameters
454
+ # => [[:keyreq, :server]]
455
+ ```
456
+
457
+ Fixture dependencies are extracted from fixture blocks:
458
+
459
+ ```ruby
460
+ fixture :client do |server:|
461
+ end
462
+ ```
463
+
464
+ Test fixture usage is extracted from test blocks:
465
+
466
+ ```ruby
467
+ test("name") do |client:|
468
+ end
469
+ ```
470
+
471
+ MVP rule:
472
+
473
+ - `:keyreq` means fixture dependency or fixture usage
474
+ - positional parameters are invalid
475
+ - optional keyword parameters are not required for MVP
476
+
477
+ Future rule:
478
+
479
+ - `:key` may mean optional fixture injection
480
+ - if fixture exists, inject it
481
+ - otherwise let Ruby default value apply
482
+
483
+ ## Invalid positional parameters
484
+
485
+ Smartest should reject this in tests:
486
+
487
+ ```ruby
488
+ test("bad") do |user|
489
+ end
490
+ ```
491
+
492
+ and this in fixtures:
493
+
494
+ ```ruby
495
+ fixture :client do |server|
496
+ end
497
+ ```
498
+
499
+ Reason:
500
+
501
+ - positional injection is ambiguous
502
+ - keyword injection is explicit
503
+ - the API should remain sharp
504
+
505
+ Suggested error for test:
506
+
507
+ ```text
508
+ Positional fixture parameters are not supported.
509
+
510
+ Use keyword fixture injection:
511
+
512
+ test("bad") do |user:|
513
+ ...
514
+ end
515
+ ```
516
+
517
+ Suggested error for fixture:
518
+
519
+ ```text
520
+ Positional fixture dependencies are not supported.
521
+
522
+ Use keyword fixture dependencies:
523
+
524
+ fixture :client do |server:|
525
+ ...
526
+ end
527
+ ```
528
+
529
+ ## Duplicate fixtures
530
+
531
+ If multiple registered fixture classes define the same fixture name, Smartest should fail.
532
+
533
+ Example:
534
+
535
+ ```ruby
536
+ class UserFixture < Smartest::Fixture
537
+ fixture :user do
538
+ end
539
+ end
540
+
541
+ class AdminFixture < Smartest::Fixture
542
+ fixture :user do
543
+ end
544
+ end
545
+
546
+ use_fixture UserFixture
547
+ use_fixture AdminFixture
548
+ ```
549
+
550
+ Error:
551
+
552
+ ```text
553
+ duplicate fixture: user
554
+ defined in:
555
+ UserFixture
556
+ AdminFixture
557
+ ```
558
+
559
+ Detection should happen when a `FixtureSet` is created.
560
+
561
+ ## Circular dependencies
562
+
563
+ This should fail:
564
+
565
+ ```ruby
566
+ fixture :a do |b:|
567
+ b
568
+ end
569
+
570
+ fixture :b do |a:|
571
+ a
572
+ end
573
+ ```
574
+
575
+ Error:
576
+
577
+ ```text
578
+ circular fixture dependency: a -> b -> a
579
+ ```
580
+
581
+ Implementation uses a resolving stack:
582
+
583
+ ```ruby
584
+ @resolving = []
585
+
586
+ def resolve(name)
587
+ return @cache[name] if @cache.key?(name)
588
+
589
+ if @resolving.include?(name)
590
+ raise CircularFixtureDependencyError
591
+ end
592
+
593
+ @resolving << name
594
+ # resolve
595
+ ensure
596
+ @resolving.pop if @resolving.last == name
597
+ end
598
+ ```
599
+
600
+ ## Fixture class inheritance
601
+
602
+ Fixture classes should support inheritance.
603
+
604
+ Example:
605
+
606
+ ```ruby
607
+ class RailsFixture < Smartest::Fixture
608
+ fixture :app do
609
+ Rails.application
610
+ end
611
+ end
612
+
613
+ class UserFixture < RailsFixture
614
+ fixture :user do
615
+ User.create!(name: "Alice")
616
+ end
617
+ end
618
+ ```
619
+
620
+ `UserFixture.fixture_definitions` should include both `:app` and `:user`.
621
+
622
+ Implementation approach:
623
+
624
+ ```ruby
625
+ def self.fixture_definitions
626
+ inherited =
627
+ if superclass.respond_to?(:fixture_definitions)
628
+ superclass.fixture_definitions
629
+ else
630
+ {}
631
+ end
632
+
633
+ inherited.merge(@fixture_definitions || {})
634
+ end
635
+ ```
636
+
637
+ Child definitions override parent definitions with the same name within the inheritance chain.
638
+
639
+ Duplicate detection applies across registered fixture classes, not parent-child internal merging.
640
+
641
+ ## Fixture instances
642
+
643
+ Each test gets fresh fixture class instances for test-scoped fixtures.
644
+
645
+ ```text
646
+ test A
647
+ WebFixture.new
648
+ cache: {}
649
+
650
+ test B
651
+ WebFixture.new
652
+ cache: {}
653
+ ```
654
+
655
+ This prevents instance variable leakage between tests.
656
+
657
+ Fixture block execution happens on the fixture instance:
658
+
659
+ ```ruby
660
+ fixture_instance.instance_exec(**dependencies, &definition.block)
661
+ ```
662
+
663
+ This allows fixture helper methods and `cleanup` to be private instance methods.
664
+
665
+ ## Helper methods in fixtures
666
+
667
+ Fixture classes may have helper methods:
668
+
669
+ ```ruby
670
+ class AppFixture < Smartest::Fixture
671
+ fixture :user do
672
+ create_user
673
+ end
674
+
675
+ private
676
+
677
+ def create_user
678
+ User.create!(name: "Alice")
679
+ end
680
+ end
681
+ ```
682
+
683
+ Fixture blocks can call private methods because they execute with `instance_exec`.
684
+
685
+ Fixture classes may optionally delegate missing methods to the execution context.
686
+
687
+ This is useful for integration helpers.
688
+
689
+ Example:
690
+
691
+ ```ruby
692
+ fixture :logged_in_user do |user:|
693
+ login_as(user)
694
+ user
695
+ end
696
+ ```
697
+
698
+ If `login_as` is defined on `ExecutionContext`, `Fixture#method_missing` may delegate to it.
699
+
700
+ This should be used carefully. Fixture dependencies themselves should still be keyword arguments, not method-missing calls.
701
+
702
+ ## Expectations
703
+
704
+ MVP expectation API:
705
+
706
+ ```ruby
707
+ expect(actual).to eq(expected)
708
+ expect(actual).not_to eq(expected)
709
+ ```
710
+
711
+ Internal model:
712
+
713
+ ```text
714
+ expect(actual)
715
+ => ExpectationTarget
716
+
717
+ eq(expected)
718
+ => EqMatcher
719
+
720
+ ExpectationTarget#to(matcher)
721
+ => matcher.match!(actual)
722
+ ```
723
+
724
+ Example:
725
+
726
+ ```ruby
727
+ class Smartest::ExpectationTarget
728
+ def initialize(actual)
729
+ @actual = actual
730
+ end
731
+
732
+ def to(matcher)
733
+ matcher.match!(@actual)
734
+ end
735
+
736
+ def not_to(matcher)
737
+ matcher.not_match!(@actual)
738
+ end
739
+ end
740
+ ```
741
+
742
+ Assertion failures should raise `Smartest::AssertionFailed`.
743
+
744
+ ## Reporter
745
+
746
+ The initial reporter should be simple.
747
+
748
+ Example output:
749
+
750
+ ```text
751
+ Running 3 tests
752
+
753
+ ✓ factorial
754
+ ✓ GET /health
755
+ ✗ GET /me
756
+
757
+ Failures:
758
+
759
+ 1) GET /me
760
+ expected 500 to eq 200
761
+
762
+ 3 tests, 2 passed, 1 failed
763
+ ```
764
+
765
+ Future reporters may include:
766
+
767
+ - documentation reporter
768
+ - dot reporter
769
+ - JSON reporter
770
+ - GitHub Actions reporter
771
+
772
+ ## CLI
773
+
774
+ The CLI should support:
775
+
776
+ ```bash
777
+ bundle exec smartest
778
+ ```
779
+
780
+ If no paths are given:
781
+
782
+ ```bash
783
+ bundle exec smartest
784
+ ```
785
+
786
+ should default to:
787
+
788
+ ```text
789
+ smartest/**/*_test.rb
790
+ ```
791
+
792
+ CLI flow:
793
+
794
+ ```ruby
795
+ require "smartest"
796
+
797
+ Kernel.include Smartest::DSL
798
+ $LOAD_PATH.unshift File.expand_path("smartest", Dir.pwd)
799
+
800
+ files = ARGV.empty? ? Dir["smartest/**/*_test.rb"] : ARGV
801
+ files.each { |file| require File.expand_path(file) }
802
+
803
+ exit Smartest::Runner.new.run
804
+ ```
805
+
806
+ `smartest/autorun` should use `at_exit`.
807
+
808
+ ```ruby
809
+ require "smartest"
810
+
811
+ Kernel.include Smartest::DSL
812
+
813
+ at_exit do
814
+ exit Smartest::Runner.new.run
815
+ end
816
+ ```
817
+
818
+ Care must be taken not to run twice if both CLI and autorun are used.
819
+
820
+ ## Exit status
821
+
822
+ - all tests passed: `0`
823
+ - any test failed: `1`
824
+ - configuration/load error: `1`
825
+ - interrupted: re-raise or exit non-zero
826
+
827
+ ## Metadata
828
+
829
+ `test` should accept metadata:
830
+
831
+ ```ruby
832
+ test("name", skip: true) do
833
+ end
834
+
835
+ test("name", tags: [:db]) do
836
+ end
837
+ ```
838
+
839
+ MVP can store metadata without implementing all behavior.
840
+
841
+ Useful metadata later:
842
+
843
+ - `skip: true`
844
+ - `only: true`
845
+ - `tags: [:db]`
846
+ - `timeout: 5`
847
+
848
+ ## Hooks
849
+
850
+ Hooks are separate from fixtures.
851
+
852
+ Potential API:
853
+
854
+ ```ruby
855
+ before do
856
+ DatabaseCleaner.start
857
+ end
858
+
859
+ after do
860
+ DatabaseCleaner.clean
861
+ end
862
+ ```
863
+
864
+ Hooks should run around each test.
865
+
866
+ Order:
867
+
868
+ ```text
869
+ before hooks
870
+ fixture setup
871
+ test body
872
+ fixture cleanup
873
+ after hooks
874
+ ```
875
+
876
+ Alternative order:
877
+
878
+ ```text
879
+ fixture setup
880
+ before hooks
881
+ test body
882
+ after hooks
883
+ fixture cleanup
884
+ ```
885
+
886
+ This needs a final decision later.
887
+
888
+ For MVP, hooks can be omitted.
889
+
890
+ Fixture cleanup already handles resource-specific teardown.
891
+
892
+ ## Scoping
893
+
894
+ Fixtures are test-scoped by default.
895
+
896
+ Every test gets fresh test-scoped fixture instances and fixture values.
897
+
898
+ Expensive shared resources can use `suite_fixture`:
899
+
900
+ ```ruby
901
+ suite_fixture :server do
902
+ server = TestServer.start
903
+ cleanup { server.stop }
904
+ server
905
+ end
906
+ ```
907
+
908
+ Supported scopes:
909
+
910
+ - `:test`
911
+ - `:suite`
912
+
913
+ `fixture :name do ... end` creates a test-scoped fixture.
914
+
915
+ `suite_fixture :name do ... end` creates a suite-scoped fixture. It is lazy:
916
+ setup runs the first time a test requests it, and cleanup runs after all tests.
917
+
918
+ Test-scoped fixtures may depend on suite-scoped fixtures. Suite-scoped fixtures
919
+ may depend only on other suite-scoped fixtures.
920
+
921
+ File-scoped fixtures are not implemented.
922
+
923
+ ## Parallel execution
924
+
925
+ MVP should not support parallel execution.
926
+
927
+ Current design can later support parallel execution if:
928
+
929
+ - each worker has an isolated suite or immutable suite definition
930
+ - each test has its own fixture set
931
+ - reporters are made thread/process safe
932
+ - global DSL registration is controlled
933
+
934
+ ## Why class-based fixtures?
935
+
936
+ Top-level fixture definitions are simple:
937
+
938
+ ```ruby
939
+ fixture(:user) do
940
+ end
941
+ ```
942
+
943
+ But class-based fixtures are more Ruby-like for larger suites.
944
+
945
+ Benefits:
946
+
947
+ - grouping
948
+ - inheritance
949
+ - private helper methods
950
+ - reusable fixture modules
951
+ - clearer organization
952
+ - fewer global definitions
953
+ - natural place for cleanup helper
954
+
955
+ Example:
956
+
957
+ ```ruby
958
+ class WebFixture < Smartest::Fixture
959
+ fixture :server do
960
+ end
961
+
962
+ private
963
+
964
+ def build_url(path)
965
+ end
966
+ end
967
+ ```
968
+
969
+ ## Fixture definition styles considered
970
+
971
+ ### Plain public methods
972
+
973
+ ```ruby
974
+ class AppFixture < Smartest::Fixture
975
+ def user
976
+ User.create!
977
+ end
978
+ end
979
+ ```
980
+
981
+ Pros:
982
+
983
+ - very Ruby-like
984
+ - excellent editor support
985
+ - easy helper composition
986
+
987
+ Cons:
988
+
989
+ - unclear which public methods are fixtures
990
+ - harder to list fixtures
991
+ - harder to detect duplicates
992
+ - harder to attach metadata
993
+ - caching is less obvious
994
+
995
+ ### `fixture :name do`
996
+
997
+ Chosen:
998
+
999
+ ```ruby
1000
+ class AppFixture < Smartest::Fixture
1001
+ fixture :user do
1002
+ User.create!
1003
+ end
1004
+ end
1005
+ ```
1006
+
1007
+ Pros:
1008
+
1009
+ - explicit fixture declaration
1010
+ - easy metadata later
1011
+ - easy dependency extraction
1012
+ - easy duplicate detection
1013
+ - easy source locations
1014
+ - easy cleanup integration
1015
+
1016
+ ### `fixture def user`
1017
+
1018
+ Considered:
1019
+
1020
+ ```ruby
1021
+ fixture def user
1022
+ User.create!
1023
+ end
1024
+ ```
1025
+
1026
+ Pros:
1027
+
1028
+ - clever Ruby syntax
1029
+ - method-like
1030
+
1031
+ Cons:
1032
+
1033
+ - surprising
1034
+ - formatter/tooling concerns
1035
+ - less obvious for users
1036
+
1037
+ Not chosen for MVP.
1038
+
1039
+ ## Resource fixtures considered
1040
+
1041
+ Considered:
1042
+
1043
+ ```ruby
1044
+ resource :server do |use|
1045
+ server = TestServer.start
1046
+ use.call(server)
1047
+ ensure
1048
+ server&.stop
1049
+ end
1050
+ ```
1051
+
1052
+ Not chosen for MVP.
1053
+
1054
+ Reason:
1055
+
1056
+ - requires around-chain execution
1057
+ - complicates dependency handling
1058
+ - not needed if `cleanup` exists
1059
+ - makes fixture API more complex
1060
+
1061
+ Could be added later as advanced API.
1062
+
1063
+ ## Final MVP API
1064
+
1065
+ ```ruby
1066
+ # smartest/test_helper.rb
1067
+ require "smartest/autorun"
1068
+
1069
+ Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
1070
+ require fixture_file
1071
+ end
1072
+ ```
1073
+
1074
+ ```ruby
1075
+ # smartest/fixtures/app_fixture.rb
1076
+ class AppFixture < Smartest::Fixture
1077
+ fixture :user do
1078
+ User.create!(name: "Alice")
1079
+ end
1080
+
1081
+ fixture :server do
1082
+ server = TestServer.start
1083
+ cleanup { server.stop }
1084
+ server
1085
+ end
1086
+
1087
+ fixture :client do |server:|
1088
+ Client.new(base_url: server.url)
1089
+ end
1090
+ end
1091
+ ```
1092
+
1093
+ ```ruby
1094
+ # smartest/example_test.rb
1095
+ require "test_helper"
1096
+
1097
+ use_fixture AppFixture
1098
+
1099
+ test("GET /health") do |client:|
1100
+ expect(client.get("/health").status).to eq(200)
1101
+ end
1102
+ ```
1103
+
1104
+ ## Future ideas
1105
+
1106
+ Possible future features:
1107
+
1108
+ - `skip`
1109
+ - `only`
1110
+ - tags
1111
+ - custom reporters
1112
+ - JSON output
1113
+ - richer matchers
1114
+ - block expectations
1115
+ - `raise_error`
1116
+ - hooks
1117
+ - file-scoped fixtures
1118
+ - parallel execution
1119
+ - watch mode
1120
+ - Rails integration
1121
+ - Capybara integration
1122
+ - Playwright/Puppeteer integration
1123
+ - snapshot assertions
1124
+ - fixture graph visualization
1125
+
1126
+ ## Design principles
1127
+
1128
+ 1. Prefer explicit fixture names.
1129
+ 2. Prefer Ruby keyword arguments over positional fixture injection.
1130
+ 3. Keep fixture teardown optional.
1131
+ 4. Keep fixture values test-scoped by default.
1132
+ 5. Avoid global mutable state except the active suite used by the DSL.
1133
+ 6. Keep MVP small.
1134
+ 7. Make errors helpful.
1135
+ 8. Do not copy RSpec's object model unless needed.
1136
+ 9. Do not copy pytest syntax blindly; adapt it to Ruby.
1137
+ 10. Make the common case beautiful.