Skip to content

Commit

Permalink
Optimize test suite, CI config (#866)
Browse files Browse the repository at this point in the history
Optimizes the test suite and CI config for performance

### Local improvements:

Speeds up local test runs by ~95% (given sufficient parallelization — 7:14 to 0:21 on my machine):

- Adds `parallel_tests` to parallelize the test suite (best results found at 1 process per core, details below)
- Adds `spring` to pre-load the application. Also speeds up rake, non-parallel test runs, etc
- Changes database cleaner strategy to default to transactions ([granularly overridable](https://github.com/bikeindex/bike_index/blob/538a1d924e43e18556069580c1cedc34c852aeb7/spec/support/database_cleaner.rb)) [more on [db cleaning strategies](https://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886#11423886)]

### CI config changes

- Reduces parallelism to [2 nodes](538a1d9), which improves throughput across multiple builds (see below)
- Caches [more granularly](a89c345) (separate caches for Ruby and Node dependencies, and for precompiled assets)

### RSpec updates 

In line with upstream deprecations:

- Refactors to remove global render_views setting
- Disables monkey patching
- Disables inferring test type from directory
- Use "rails_helper" instead of "spec_helper" (the latter should be used for any non-rails code)

### Usage

To run the RSpec suite in parallel, set up databases with 

```
bin/rake parallel:setup
```

and run with 

```
bin/rake parallel:spec
```

You can override the default number of processes with the following env vars:

```
export PARALLEL_TEST_PROCESSORS=XX   
```

Set [`export DISABLE_SPRING=0`](https://github.com/grosser/parallel_tests/wiki/Spring) to speed up app boot time when running tests in parallel.
  • Loading branch information
Jake Romer committed Jun 9, 2019
1 parent 29acdb2 commit db52a54
Show file tree
Hide file tree
Showing 274 changed files with 1,183 additions and 594 deletions.
66 changes: 48 additions & 18 deletions .circleci/config.yml
Expand Up @@ -2,13 +2,14 @@ version: 2
jobs:
build:
working_directory: ~/bikeindex/bike_index
parallelism: 5
parallelism: 2
shell: /bin/bash --login
environment:
RAILS_ENV: test
RACK_ENV: test
COVERAGE: true
TZ: /usr/share/zoneinfo/America/Chicago

docker:
- image: circleci/ruby:2.5.1-stretch-node
environment:
Expand All @@ -19,55 +20,77 @@ jobs:
POSTGRES_USER: root
POSTGRES_DB: bikeindex_test
- image: redis:4.0.9

steps:
- checkout
- restore_cache:
keys:
# This branch if available
- v2-dep-{{ .Branch }}-
# Default branch if not
- v2-dep-master-
# Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly
- v2-dep-

- run:
name: install dockerize
command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
environment:
DOCKERIZE_VERSION: v0.6.1

- run:
name: install system libraries
command: sudo apt-get update && sudo apt-get -y install imagemagick postgresql-client

- run:
name: install bundler
command: gem install bundler

# Ruby dependencies
- restore_cache:
key: v2-bundler-{{ checksum "Gemfile.lock" }}
- run:
name: bundle gems
command: bundle install --path=vendor/bundle --jobs=4 --retry=3
# So that we can compile assets, since we use node & yarn
- save_cache:
key: v2-bundler-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
- ~/.bundle

# Node dependencies
- restore_cache:
key: v2-yarn-{{ checksum "yarn.lock" }}
- run:
name: Yarn Install
command: yarn install --cache-folder ~/.cache/yarn
- save_cache:
key: v2-yarn-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn

# Asset compilation
- restore_cache:
keys:
# This branch if available
- v2-assets-{{ .Branch }}
# Default branch if not
- v2-assets-master-
- v2-assets-
- run: bundle exec rake assets:precompile
- save_cache:
key: v2-assets-{{ .Branch }}
paths:
- public/assets
- tmp/cache/assets/sprockets

- run:
name: Install Code Climate Test Reporter
command: |
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
- run:
name: Wait for PostgreSQL to start
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- save_cache:
key: v2-dep-{{ .Branch }}-{{ epoch }}
paths:
- ./vendor/bundle
- ~/.bundle
- public/assets
- tmp/cache/assets/sprockets
- ~/.cache/yarn

- run:
name: Setup Database
command: |
bundle exec rake db:create db:structure:load
- run:
name: RSpec
command: |
Expand All @@ -77,31 +100,38 @@ jobs:
bundle exec rspec --profile 10 \
--color \
--order random \
--require rails_helper \
--format RspecJunitFormatter \
--out test-results/rspec/rspec.xml \
--format progress \
-- ${TESTFILES}
- run:
name: "eslint"
command: "yarn lint"

- run:
name: "Jest: Install junit coverage"
command: yarn add --dev jest-junit

- run:
name: "Jest: Tests"
command: yarn jest --ci --runInBand --reporters=default --reporters=jest-junit
environment:
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"

- run:
name: Code Climate Test Coverage
command: |
./cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- persist_to_workspace:
root: coverage
paths:
- codeclimate.*.json

- store_test_results:
path: test-results

- store_artifacts:
path: test-artifacts

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -47,3 +47,7 @@ yarn-error.log

# Ignore jest coverage files
coverage

# Ignore test results
test-results/
spec/examples.txt
1 change: 1 addition & 0 deletions .rspec
@@ -0,0 +1 @@
--color
12 changes: 12 additions & 0 deletions .rspec_parallel
@@ -0,0 +1,12 @@
--require rails_helper

--no-profile
--order random

--format progress

--format ParallelTests::RSpec::SummaryLogger
--out tmp/parallel_spec_summary.log

--format ParallelTests::RSpec::RuntimeLogger
--out tmp/parallel_runtime_rspec.log
6 changes: 5 additions & 1 deletion Gemfile
Expand Up @@ -68,7 +68,7 @@ gem "twitter" # Twitter. For rendering tweets
# OAuth provider, Grape, associated parts of API V2
gem "api-pagination"
gem "doorkeeper", "~> 3.1.0"
gem "grape", "~> 0.14.0"
gem "grape", "~> 0.19.1"
gem "grape-active_model_serializers", "~> 1.4.0"
gem "grape-swagger", "~> 0.10.4"
gem "swagger-ui_rails"
Expand Down Expand Up @@ -130,13 +130,17 @@ group :development do
gem "bullet"
gem "letter_opener"
gem "rerun"
gem "spring"
gem "spring-commands-rspec"
gem "spring-commands-rubocop"
end

group :development, :test do
gem "database_cleaner"
gem "dotenv-rails"
gem "foreman"
gem "jazz_fingers"
gem "parallel_tests"
gem "pry-byebug"
gem "pry-rails"
gem "rb-fsevent", "~> 0.9.1"
Expand Down
23 changes: 18 additions & 5 deletions Gemfile.lock
Expand Up @@ -174,15 +174,15 @@ GEM
git-version-bump (0.15.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
grape (0.14.0)
grape (0.19.2)
activesupport
builder
hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
rack-mount
virtus (>= 1.0.0)
grape-active_model_serializers (1.4.0)
active_model_serializers (>= 0.9.0)
Expand Down Expand Up @@ -316,6 +316,9 @@ GEM
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
mustermann (1.0.3)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
naught (1.1.0)
nenv (0.3.0)
netrc (0.11.0)
Expand All @@ -342,6 +345,8 @@ GEM
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
parallel (1.17.0)
parallel_tests (2.29.0)
parallel
paranoia (2.1.5)
activerecord (~> 4.0)
parser (2.6.2.1)
Expand Down Expand Up @@ -376,8 +381,6 @@ GEM
rack (~> 1.4)
rack-mini-profiler (0.10.1)
rack (>= 1.2.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
rack-protection (1.5.5)
rack
rack-proxy (0.6.5)
Expand Down Expand Up @@ -538,6 +541,12 @@ GEM
redis (~> 3.0, >= 3.0.5)
sinatra (>= 1.4.4)
vegas (>= 0.1.0)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.2.0)
spring (>= 1.0, < 3.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
Expand Down Expand Up @@ -638,7 +647,7 @@ DEPENDENCIES
fog-aws
foreman
geocoder
grape (~> 0.14.0)
grape (~> 0.19.1)
grape-active_model_serializers (~> 1.4.0)
grape-swagger (~> 0.10.4)
grape_logging
Expand Down Expand Up @@ -669,6 +678,7 @@ DEPENDENCIES
omniauth (~> 1.6)
omniauth-facebook
omniauth-strava
parallel_tests
paranoia
pg
pg_search
Expand Down Expand Up @@ -714,6 +724,9 @@ DEPENDENCIES
sitemap_generator (~> 6)
skylight
soulheart (~> 0.3.0)
spring
spring-commands-rspec
spring-commands-rubocop
sprockets-rails (~> 3.0.4)
stackprof
stripe
Expand Down
61 changes: 39 additions & 22 deletions Guardfile
@@ -1,27 +1,44 @@
group :red_green_refactor, halt_on_fail: true do
guard :rspec, cmd: 'bundle exec rspec', failed_mode: :focus do
rspec_options = {
cmd: "bin/rspec -f progress",
cmd_additional_args: "--require rails_helper --no-profile --order defined",
run_all: {
cmd: "bin/parallel_rspec --quiet --test-options='-f documentation -o /dev/null -f progress",
cmd_additional_args: "'",
},
failed_mode: :focus,
}

guard :rspec, rspec_options do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch(%r{^config/initializers/(.+)\.rb$}) { |m| "spec/initializers/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
watch(%r{^app/controllers/api/v2/(.+)\.rb$}) { |m| "spec/requests/api/v2/#{m[1]}_spec.rb"}
watch(%r{^app/controllers/api/v3/(.+)\.rb$}) { |m| "spec/requests/api/v3/#{m[1]}_spec.rb"}
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch(%r{^config/initializers/(.+)\.rb$}) { |m| "spec/initializers/#{m[1]}_spec.rb" }

watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/controllers/#{m[1]}_#{m[2]}_spec.rb" }
# watch(%r{^spec/support/(.+)\.rb$}) { "spec" } # Stop running all specs on shared_example update
watch('config/routes.rb') { "spec/routing" }
watch('app/controllers/application_controller.rb') { "spec/controllers" }
end
watch("spec/spec_helper.rb") { "spec" }
watch("spec/rails_helper.rb") { "spec" }

watch(%r{^app/controllers/api/v2/(.+)\.rb$}) { |m| "spec/requests/api/v2/#{m[1]}_spec.rb" }
watch(%r{^app/controllers/api/v3/(.+)\.rb$}) { |m| "spec/requests/api/v3/#{m[1]}_spec.rb" }

guard :rubocop, all_on_start: false do
watch(%r{^app/(.+)\.rb$})
watch(%r{^spec/(.+)\.rb$})
watch(%r{^config/(.+)\.rb$})
watch(%r{^lib/(.+)\.rb$})
watch(%r{^lib/(.+)\.rake$})
watch(%r{^db/(.+)\.rb$})
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }

watch("config/routes.rb") { "spec/routing" }
watch("app/controllers/application_controller.rb") { "spec/controllers" }

watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "spec/requests/#{m[1]}_request_spec.rb" }
watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "spec/requests/#{m[1]}_controller_spec.rb" }
end
end
end

# group :linting, halt_on_fail: true do
# guard :rubocop, all_on_start: false do
# watch(%r{^app/(.+)\.rb$})
# watch(%r{^spec/(.+)\.rb$})
# watch(%r{^config/(.+)\.rb$})
# watch(%r{^lib/(.+)\.rb$})
# watch(%r{^lib/(.+)\.rake$})
# watch(%r{^db/(.+)\.rb$})
# watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
# end
# end
2 changes: 1 addition & 1 deletion Procfile_development
@@ -1,4 +1,4 @@
web: bundle exec unicorn_rails -p 3001
webpacker: ./bin/webpack-dev-server # Live update webpack js
log: tail -f log/development.log
hard_worker: bundle exec rerun --background --dir app,db,lib --pattern '{**/*.rb}' -- bundle exec sidekiq
hard_worker: bundle exec rerun --background --dir app,db,lib --pattern '{**/*.rb}' -- bundle exec sidekiq

0 comments on commit db52a54

Please sign in to comment.