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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d08387f9cae75a24a2f735821531a38182f2ac3d918a82184f8315577139fa19
4
- data.tar.gz: 5b3397f68f5573c48c7a2fa60a5ba492c52b235e6496bfb0651902be7764cb76
3
+ metadata.gz: fdfded4c7c676dbe7670b38df98318ea3d3728252353b6da8eae9cb906cfb719
4
+ data.tar.gz: 2d786c4fbbe81c5607ccbb92d6610e5da8d5d04fe2b7287fffaa62fdcfd72a3d
5
5
  SHA512:
6
- metadata.gz: 19e6f881162c9a1e2f13421ef5df5a0877e954545adda1eb6611e27072fbebdb9407d78e430cd86cb872916f127a9b43c106c7a3c1de17de38dcf3ae40bba04c
7
- data.tar.gz: c899d637681f49ea8500f3bdc3d80490e991a2be929edf4e26ad7a789bab2c04c372dc3a9c4af2dce51cf444d709980a9997821c2cc6f1179f488cd1465ad831
6
+ metadata.gz: ab745775d4fe7da0966d0600bce51e8f18ff20dfbc948cb6a4bfbe470371a219d797389b475471a742aee57e360b57e27f4bed4d8275e5dbda8c61807cd641a8
7
+ data.tar.gz: 9aa530efeb5c6d3bc9bb368a5cf60f276766257e126c341a508400e6081baca1046fee32502e2885568b59c038bd0b07aaed6c7f846cd824a99ba340c86fbd41
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fixpoints (0.1.0)
4
+ fixpoints (0.1.1)
5
5
  activerecord (>= 5.0.0)
6
+ rspec
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
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
- TODO
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
- Link to `https://tomrothe.de/posts/behaviour-driven-test-data.html`
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
- Add this line to your application's Gemfile: `gem 'fixpoints'`
38
+ We save the fixpoint (database snapshot) after the test. Other tests can build on them.
14
39
 
15
- TODO: Write usage instructions here
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
- # TODO: update this code
20
- it 'registers a user' do
21
- visit new_user_path
22
- fill_in 'Name', with: 'Hans'
23
- click_on 'Save'
24
-
25
- store_fixpoint :registred_user
26
- # creates YAML files containing all records (/spec/fixpoints/[table_name].yml)
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
- it 'posts an item' do
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
- user = User.find_by(name: 'Hans')
33
- visit new_item_path(user)
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
- ## Development
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.
@@ -3,6 +3,7 @@ require_relative "fixpoint_diff"
3
3
 
4
4
  require_relative "fixpoint"
5
5
  require_relative "incremental_fixpoint"
6
+ require_relative "fixpoint_test_helpers"
6
7
 
7
8
  if defined?(RSpec) && RSpec.respond_to?(:configure)
8
9
  RSpec.configure { |c| c.add_setting :fixpoints_path }
@@ -1,3 +1,3 @@
1
1
  module Fixpoints
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -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, &not_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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixpoints
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Rothe