fixpoints 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -1
- data/README.md +110 -28
- data/lib/fixpoints.rb +1 -0
- data/lib/fixpoints/version.rb +1 -1
- data/lib/incremental_fixpoint.rb +0 -58
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdfded4c7c676dbe7670b38df98318ea3d3728252353b6da8eae9cb906cfb719
|
4
|
+
data.tar.gz: 2d786c4fbbe81c5607ccbb92d6610e5da8d5d04fe2b7287fffaa62fdcfd72a3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab745775d4fe7da0966d0600bce51e8f18ff20dfbc948cb6a4bfbe470371a219d797389b475471a742aee57e360b57e27f4bed4d8275e5dbda8c61807cd641a8
|
7
|
+
data.tar.gz: 9aa530efeb5c6d3bc9bb368a5cf60f276766257e126c341a508400e6081baca1046fee32502e2885568b59c038bd0b07aaed6c7f846cd824a99ba340c86fbd41
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -2,46 +2,131 @@
|
|
2
2
|
|
3
3
|
Fixpoints enables saving, restoring and comparing the database state before & after tests.
|
4
4
|
|
5
|
+
This gem came about during my time at [Netskin GmbH](https://www.netskin.com/en). Check it out, we do great (Rails) work there.
|
6
|
+
|
5
7
|
## Motivation
|
6
8
|
|
7
|
-
|
9
|
+
When running behavior tests, we seed the database with a defined snapshot called fixpoint.
|
10
|
+
We do run the behavior test and save the resulting database state as another fixpoint.
|
11
|
+
This method allows testing complex business processes in legacy applications without having to implement fixtures/factories upfront.
|
12
|
+
By building one fixpoint on top of another, we can ensure that the process chain works without any gaps.
|
13
|
+
Comparing each resulting database state at the end of a test with a previously recorded state ensures that refactoring did not have unintended side effects.
|
14
|
+
|
15
|
+
**Advantages**
|
16
|
+
|
17
|
+
- No need to write fixtures or factories
|
18
|
+
- discover which records were created/changed by the test’s actions by reading the fixpoint file (YAML)
|
19
|
+
- get notified about differences in database state (i.e. unintended side effects) after refactoring something
|
20
|
+
- allow version control to save the "ground truth" at the end of a test
|
21
|
+
|
22
|
+
Please check out the full article: [Behavior-Driven Test Data](https://tomrothe.de/posts/behaviour-driven-test-data.html).
|
8
23
|
|
9
|
-
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile: `gem 'fixpoints'` and make sure to add:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# rails_helper.rb
|
30
|
+
RSpec.configure do |config|
|
31
|
+
# ...
|
32
|
+
config.include FixpointTestHelpers
|
33
|
+
end
|
34
|
+
```
|
10
35
|
|
11
36
|
## Usage
|
12
37
|
|
13
|
-
|
38
|
+
We save the fixpoint (database snapshot) after the test. Other tests can build on them.
|
14
39
|
|
15
|
-
|
40
|
+
A fixpoint is a snapshot of the database contents as YAML file.
|
41
|
+
It is saved to the `spec/fixpoints` folder.
|
42
|
+
The file contains a mapping of table names to a list if their records.
|
43
|
+
Empty tables are stripped from files.
|
16
44
|
|
45
|
+
**Order & Bootstrapping** We need to mind the order though.
|
46
|
+
When bootstrapping (when there is no fixpoints saved to the disk yet), we need to make sure that all tests that depend on a certain fixpoint run _after_ it was stored.
|
47
|
+
In a single RSpec file, you can use the order in which the tests are defined (`RSpec.describe 'MyFeature', order: :defined do`).
|
48
|
+
However, tests in groups might follow a slightly different order (see [RSpec Docs](https://relishapp.com/rspec/rspec-core/docs/configuration/overriding-global-ordering))
|
17
49
|
|
18
50
|
```ruby
|
19
|
-
|
20
|
-
it 'registers a user' do
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
51
|
+
RSpec.describe 'User Flow', order: :defined do # !!! mind the order here !!!
|
52
|
+
it 'registers a user' do
|
53
|
+
visit new_user_path
|
54
|
+
fill_in 'Name', with: 'Tom'
|
55
|
+
click_on 'Save'
|
56
|
+
|
57
|
+
store_fixpoint_unless_present :registred_user
|
58
|
+
# creates a YAML file containing all records (/spec/fixpoints/registred_user.yml)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'posts an item' do
|
62
|
+
restore_fixpoint :registered_user
|
63
|
+
|
64
|
+
user = User.find_by(name: 'Hans')
|
65
|
+
visit new_item_path(user)
|
66
|
+
fill_in 'Item', with: '...'
|
67
|
+
click_on 'Post'
|
68
|
+
|
69
|
+
compare_fixpoint(:item_posted, store_fixpoint_and_fail: true)
|
70
|
+
# compares the database state with the previously saved fixpoint and
|
71
|
+
# raises if there is a difference. when there is no previous fixpoint,
|
72
|
+
# it writes the fixpoint and fails the test (so it can be re-run)
|
73
|
+
end
|
27
74
|
end
|
75
|
+
```
|
76
|
+
|
77
|
+
**Changes** If you did a lot of changes to a test, you can remove a fixpoint file from its directory.
|
78
|
+
It will be recreated when the test producing it runs again.
|
79
|
+
Don't forget re-running the tests based on it because their fixpoints might have to change too.
|
80
|
+
Example: You need to add something to the database's `seeds.rb`. All subsequent fixpoints are missing the required entry.
|
81
|
+
To update all fixpoints, just remove the whole `spec/fixpoints` folder and re-run all tests. Now all fixpoints should be updated.
|
82
|
+
Be careful though, don't just remove the fixpoints if you are not sure what is going on.
|
83
|
+
A change in a fixpoint might point to an unintended change in code.
|
84
|
+
|
85
|
+
We need to be be careful to use `let` and `let!` with factories.
|
86
|
+
Records might be created twice when using create in there (once by the fixpoint and once by the factory).
|
87
|
+
|
88
|
+
**Ignoring columns**
|
28
89
|
|
29
|
-
|
90
|
+
Often you might want to add more columns to ignore (e.g. login time stamps):
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
let(:ignored_fixpoint_columns) { [:updated_at, :created_at, users: [:last_login_at] }
|
94
|
+
# ignores timestamps for all tables, and last_login_at for the users table
|
95
|
+
|
96
|
+
it 'logs in' do
|
30
97
|
restore_fixpoint :registered_user
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
fill_in 'Item', with: '...'
|
35
|
-
click_on 'Post'
|
36
|
-
|
37
|
-
compare_fixpoint(:posted_item, ignore_columns: [:release_date], store_fixpoint_and_fail: true)
|
38
|
-
# compares the database state with the previously saved fixpoint and
|
39
|
-
# raises if there is a difference. when there is no previous fixpoint,
|
40
|
-
# it writes it and fails the test (so it can be re-run)
|
98
|
+
# ...
|
99
|
+
compare_fixpoint(:registered_user, ignored_fixpoint_columns)
|
100
|
+
# asserts that there is no change
|
41
101
|
end
|
42
102
|
```
|
43
103
|
|
44
|
-
|
104
|
+
**Incremental** By the default the `FixpointTestHelpers` use the `IncrementalFixpoint` instead of the more verbose `Fixpoint` version.
|
105
|
+
This means that only changes are saved to the YAML file.
|
106
|
+
In order to achieve this, we must make sure that we let the store function know who daddy is.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
it 'posts an item' do
|
110
|
+
restore_fixpoint :registered_user
|
111
|
+
# ...
|
112
|
+
compare_fixpoint(fixname, store_fixpoint_and_fail: true, parent_fixname: :registered_user)
|
113
|
+
# now only changes to compared to the previous fixpoint are stored
|
114
|
+
# instead of using the name of the last restored fixpoint, you can also use `:last_restored`
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
|
119
|
+
## Limitations & Known issues
|
120
|
+
|
121
|
+
- The records in tables are ordered by their id.
|
122
|
+
If there is no id for a table, we use database's order (what the SELECT query returns).
|
123
|
+
This order may be instable.
|
124
|
+
- We do not clean the database after each test, depending on your cleaning strategy (e.g. transaction), we might leak primary key sequence counters from one test to another.
|
125
|
+
If you have problems try running `Fixpoint.reset_pk_sequences!` and create am issue, so we can investigate.
|
126
|
+
- Under certain conditions you may get `duplicate key value violates unique constraint` because the primary key sequences are not updated correctly.
|
127
|
+
If this happens, just add a `Fixpoint.reset_pk_sequences!` at the beginning of your test. We need to dig a little deeper here at some point...
|
128
|
+
|
129
|
+
# Development
|
45
130
|
|
46
131
|
```bash
|
47
132
|
docker run --rm -ti -v (pwd):/app -w /app ruby:2.7 bash
|
@@ -53,12 +138,9 @@ gem build
|
|
53
138
|
gem install fixpoints-0.1.0.gem
|
54
139
|
pry -r fixpoints
|
55
140
|
gem uninstall fixpoints
|
56
|
-
gem push
|
141
|
+
gem push fixpoints
|
57
142
|
```
|
58
143
|
|
59
|
-
|
60
|
-
## Development
|
61
|
-
|
62
144
|
## Contributing
|
63
145
|
|
64
146
|
Bug reports and pull requests are welcome on GitHub at https://github.com/motine/fixpoints.
|
data/lib/fixpoints.rb
CHANGED
data/lib/fixpoints/version.rb
CHANGED
data/lib/incremental_fixpoint.rb
CHANGED
@@ -49,61 +49,3 @@ class IncrementalFixpoint < Fixpoint
|
|
49
49
|
return YAML.dump(file_contents)
|
50
50
|
end
|
51
51
|
end
|
52
|
-
|
53
|
-
# Helper methods to be included into RSpec
|
54
|
-
module FixpointTestHelpers
|
55
|
-
def restore_fixpoint(fixname)
|
56
|
-
IncrementalFixpoint.from_file(fixname).load_into_database
|
57
|
-
end
|
58
|
-
|
59
|
-
# Compares the fixpoint with the records in the database.
|
60
|
-
# If there is no such fixpoint yet, it will write a new one to the file system.
|
61
|
-
# The latter is useful if the fixpoint was deleted to accommodate changes to it (see example in class description).
|
62
|
-
#
|
63
|
-
# +tables_to_compare+ can either be +:all+ or a list of table names (e.g. ['users', 'posts'])
|
64
|
-
# +ignored_columns+ see Fixnum#records_for_table
|
65
|
-
# +not_exists_handler+ when given and the fixpoint does not exists, it will be called with the fixname as argument
|
66
|
-
#
|
67
|
-
# ---
|
68
|
-
# If we refactor this to a gem, we should rely on rspec (e.g. use minitest or move comparison logic to Fixpoint class).
|
69
|
-
# Anyhow, we keep it like this for now, because the expectations give much nicer output than the minitest assertions.
|
70
|
-
def compare_fixpoint(fixname, ignored_columns=[:updated_at, :created_at], tables_to_compare=:all, ¬_exists_handler)
|
71
|
-
if !IncrementalFixpoint.exists?(fixname)
|
72
|
-
not_exists_handler.call(fixname) if not_exists_handler
|
73
|
-
return
|
74
|
-
end
|
75
|
-
|
76
|
-
database_fp = IncrementalFixpoint.from_database
|
77
|
-
fixpoint_fp = IncrementalFixpoint.from_file(fixname)
|
78
|
-
|
79
|
-
tables_to_compare = (database_fp.table_names + fixpoint_fp.table_names).uniq if tables_to_compare == :all
|
80
|
-
tables_to_compare.each do |table_name|
|
81
|
-
db_records = database_fp.records_for_table(table_name, ignored_columns)
|
82
|
-
fp_records = fixpoint_fp.records_for_table(table_name, ignored_columns)
|
83
|
-
|
84
|
-
# if a table is present in a fixpoint, there must be records in it because empty tables are stripped from fixpoints
|
85
|
-
expect(db_records).not_to be_empty, "#{table_name} not in database, but in fixpoint"
|
86
|
-
expect(fp_records).not_to be_empty, "#{table_name} not in fixpoint, but in database"
|
87
|
-
# we assume that the order of records returned by SELECT is stable (so we do not do any sorting)
|
88
|
-
expect(db_records).to eq(fp_records), "Database records for table \"#{table_name}\" did not match fixpoint \"#{fixname}\". Consider removing the fixpoint and re-running the test if the change is intended."
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def store_fixpoint_and_fail(fixname, parent_fixname = nil)
|
93
|
-
store_fixpoint(fixname, parent_fixname)
|
94
|
-
pending("Fixpoint \"#{fixname}\" did not exist yet. Skipping comparison, but created fixpoint from database")
|
95
|
-
fail
|
96
|
-
end
|
97
|
-
|
98
|
-
# it is not a good idea to overwrite the fixpoint each time because timestamps may change (which then shows up in version control).
|
99
|
-
# Hence we only provide a method to write to it if it does not exist.
|
100
|
-
def store_fixpoint_unless_present(fixname, parent_fixname = nil)
|
101
|
-
store_fixpoint(fixname, parent_fixname) unless IncrementalFixpoint.exists?(fixname)
|
102
|
-
end
|
103
|
-
|
104
|
-
# +parent_fixname+ when given, only the (incremental) changes to the parent are saved
|
105
|
-
# please see store_fixpoint_unless_present for note on why not to use this method
|
106
|
-
def store_fixpoint(fixname, parent_fixname = nil)
|
107
|
-
IncrementalFixpoint.from_database(parent_fixname).save_to_file(fixname)
|
108
|
-
end
|
109
|
-
end
|