<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Zero Config Rails Blog]]></title><description><![CDATA[`rails new` on steroids

Zero Config Rails allows you to create new Rails applications in less than 30 minutes, fully automating all tedious gem configurations ]]></description><link>https://blog.zeroconfigrails.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1726636722916/9dee1b3b-a7f8-471d-91d0-9997e8728c4f.png</url><title>The Zero Config Rails Blog</title><link>https://blog.zeroconfigrails.com</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 15:04:22 GMT</lastBuildDate><atom:link href="https://blog.zeroconfigrails.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Setup RSpec Tests in Rails with Gitlab CI]]></title><description><![CDATA[At Zero Config Rails, I (Prabin) am constantly working on automating configurations and boring setups like “Setup RSpec Tests in Rails with Gitlab CI”.
CI integrations helps in improving the code quality in projects. It can help in automating code re...]]></description><link>https://blog.zeroconfigrails.com/setup-rspec-tests-in-rails-with-gitlab-ci</link><guid isPermaLink="true">https://blog.zeroconfigrails.com/setup-rspec-tests-in-rails-with-gitlab-ci</guid><category><![CDATA[Rails]]></category><category><![CDATA[#rspec]]></category><category><![CDATA[Testing]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[GitLab-CI]]></category><dc:creator><![CDATA[Prabin Poudel]]></dc:creator><pubDate>Sat, 05 Oct 2024 13:41:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728135403636/5197d7c5-4818-4dfb-97b9-44781a1ca2bd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At <a target="_blank" href="https://zeroconfigrails.com">Zero Config Rails</a>, I (<a target="_blank" href="https://x.com/coolprobn">Prabin</a>) am constantly working on automating configurations and boring setups like “Setup RSpec Tests in Rails with Gitlab CI”.</p>
<p>CI integrations helps in improving the code quality in projects. It can help in automating code reviews for linting and standard practices as well as for running tests to check if code change breaks any existing functionalities.</p>
<p>Today we will look at adding configurations to Gitlab CI for running RSpec tests in our Rails application.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can configure the Gitlab CI for RSpec with a single command using Zero Config Rails. Just hit the following command to get the complete setup: <code>gem install zcr-zen &amp;&amp; zen add ci:gitlab_ci --app_test_framework=rspec</code></div>
</div>

<p>For the detailed list of configurations, you can visit <a target="_blank" href="https://generators.zeroconfigrails.com/install/gitlab_ci">Gitlab CI Generator</a>.</p>
<p>Now without further ado, let’s jump right into setting up Gitlab CI for RSpec and run those tests in CI.</p>
<h2 id="heading-assumption">Assumption</h2>
<ul>
<li><p>You have basic Gitlab CI configurations ready i.e. <code>.gitlab-ci.yml</code> exists in your project. If it doesn't, you can refer to my other article <a target="_blank" href="https://prabinpoudel.com.np/articles/integrate-pronto-with-gitlab-ci-for-rails-app/">Integrate Pronto with Gitlab for Rails App</a>.</p>
</li>
<li><p>You are using PostgreSQL in your app though with minimal changes it should also work for any other databases.</p>
</li>
</ul>
<h2 id="heading-tested-and-working-in">Tested and working in</h2>
<ul>
<li><p>Ruby 3.3.0</p>
</li>
<li><p>Rails 7.1.3</p>
</li>
<li><p>rspec-rails 6.1.1</p>
</li>
<li><p>selenium-webdriver 4.18.1</p>
</li>
</ul>
<h2 id="heading-configure-gitlab-ci-variables">Configure Gitlab CI Variables</h2>
<p>Firs of all, we need to add some configurations required by the CI to run tests. This should be done over at <a target="_blank" href="https://gitlab.com">Gitlab</a>.</p>
<h3 id="heading-add-variable-for-storing-environment-variables">Add variable for storing environment variables</h3>
<p>I normally use Figjam which is a maintained version of the popular Figaro gem for storing environment variables which uses <code>config/application.yml</code> but just the plain <code>.env</code> file using dotenv gem is also very popular. Anyway, just copy the content from whatever you are using and paste it inside the Value for this new variable.</p>
<p>You can visit the <a target="_blank" href="https://docs.gitlab.com/ee/ci/variables/#project-cicd-variables">official documentation</a> to learn about setting up variables for Gitlab CI. You have to go to your project's setting in Gitlab and configure these in CI/CD variables.</p>
<p>Create a new variable for storing content in your <code>config/application.yml</code>:</p>
<ol>
<li><p>Type: File</p>
</li>
<li><p>Flags</p>
<p> Uncheck all checklists here i.e. Protect variable, Mask variable and Expand variable reference</p>
</li>
<li><p>Description</p>
<p> You can add “Environment Variables“ but it's optional and you can skip this as "Key" (just below this) is already clear enough on what this variable is storing.</p>
</li>
<li><p>Key: <code>env</code></p>
</li>
</ol>
<p>In "Value", add the copied content from your env file.</p>
<p><em>NOTE</em>: Make sure to only copy what is under "test" block or ".env.test", you don’t want to add production variables here!</p>
<h3 id="heading-add-variable-for-masterkey">Add variable for <code>MASTER_KEY</code></h3>
<p>Rails comes with <code>config/credentials.yml.enc</code> for storing secrets, we generally also use ENV variables for this but since Rails credentials is the default, we will also look at how to configure those.</p>
<p>To decrypt credentials file, you need MASTER_KEY. If you have generated multiple credentials file per environment then you might have multiple keys like master.key, staging.key, production.key, etc..</p>
<p>Create a new variable for storing content in the “.key” file that can decrypt secrets configured for the test environment; normally this will be inside the <code>config/master.key</code>:</p>
<ol>
<li><p>Type: Variable (Default)</p>
</li>
<li><p>Flags</p>
<p> Uncheck all checklists here i.e. Protect variable, Mask variable and Expand variable reference</p>
</li>
<li><p>Description</p>
<p> Optional. You can leave it blank.</p>
</li>
<li><p>Key: MASTER_KEY</p>
</li>
</ol>
<p>And in Value, add the content copied from the <code>master.key</code></p>
<h2 id="heading-add-databaseymlci-file">Add <code>database.yml.ci</code> file</h2>
<p>It's not considered a good practice to use <code>config/database.yml</code> file for the CI so we will instead create a new file <code>config/database.yml.ci</code> and add configurations required to run tests inside.</p>
<p>You can visit the <a target="_blank" href="https://docs.gitlab.com/ee/ci/variables/#project-cicd-variables">official documentation</a> to learn about setting up variables. You have to go to your project's setting in Gitlab and configure these in CI/CD variables.</p>
<p>After creating the file, add the following:</p>
<pre><code class="lang-yml"><span class="hljs-attr">test:</span>
  <span class="hljs-attr">adapter:</span> <span class="hljs-string">postgresql</span>
  <span class="hljs-attr">encoding:</span> <span class="hljs-string">unicode</span>
  <span class="hljs-attr">host:</span> <span class="hljs-string">postgres</span>
  <span class="hljs-attr">database:</span> <span class="hljs-string">ci_db</span>
  <span class="hljs-attr">username:</span> <span class="hljs-string">postgres</span>
  <span class="hljs-attr">pool:</span> &lt;%=<span class="ruby"> ENV.fetch(<span class="hljs-string">"RAILS_MAX_THREADS"</span>) { <span class="hljs-number">5</span> } </span>%&gt;
</code></pre>
<p>For username it should be “postgres” which is the default user that gets created when postgres service/docker is created hence it doesn’t ask password or tries to authenticate user. You might get an error otherwise because no other users will have been created in postgres at this point: `Please check your database configuration to ensure the username/password are valid`.</p>
<p>For host make sure to use "postgres" instead of “localhost”. For MySQL, you will have to use "mysql" as said in the <a target="_blank" href="https://docs.gitlab.com/ee/ci/services/index.html#how-services-are-linked-to-the-job">official documentation</a>:</p>
<blockquote>
<p>The service container for MySQL is accessible under the hostname mysql. To access your database service, connect to the host named mysql instead of a socket or localhost.</p>
</blockquote>
<p>SQLite doesn’t need any host configurations but other configurations will most probably vary.</p>
<h2 id="heading-configure-capybara-with-selenium">Configure Capybara with Selenium</h2>
<p>We will configure Selenium with Chrome to be used both in CI and Local with Headless mode (by default) while also allowing to run in the browser if needed for debugging.</p>
<p>Create a new file "spec/support/capybara.rb" and add the following code:</p>
<pre><code class="lang-ruby">Capybara.register_driver <span class="hljs-symbol">:selenium_chrome_custom</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|app|</span>
  options = Selenium::WebDriver::Chrome::Options.new

  options.add_argument(<span class="hljs-string">"--headless=new"</span>) <span class="hljs-keyword">unless</span> ENV[<span class="hljs-string">"SELENIUM_HEADFUL"</span>]

  options.add_argument(<span class="hljs-string">"--window-size=1400,1400"</span>)
  options.add_argument(<span class="hljs-string">"--no-sandbox"</span>)
  options.add_argument(<span class="hljs-string">"--disable-dev-shm-usage"</span>)

  remote_url = ENV[<span class="hljs-string">"SELENIUM_REMOTE_URL"</span>]

  <span class="hljs-keyword">if</span> remote_url
    Capybara::Selenium::Driver.new(
      app,
      <span class="hljs-symbol">browser:</span> <span class="hljs-symbol">:remote</span>,
      <span class="hljs-symbol">url:</span> remote_url,
      <span class="hljs-symbol">options:</span>
    )
  <span class="hljs-keyword">else</span>
    Capybara::Selenium::Driver.new(app, <span class="hljs-symbol">browser:</span> <span class="hljs-symbol">:chrome</span>, <span class="hljs-symbol">options:</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

RSpec.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.before(<span class="hljs-symbol">:each</span>, <span class="hljs-symbol">type:</span> <span class="hljs-symbol">:system</span>, <span class="hljs-symbol">js:</span> <span class="hljs-literal">true</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># Make the test app listen to outside requests, required for the remote Selenium instance</span>
    Capybara.server_host = <span class="hljs-string">"0.0.0.0"</span>
    Capybara.server_port = <span class="hljs-number">3000</span>

    <span class="hljs-keyword">if</span> ENV[<span class="hljs-string">"SELENIUM_REMOTE_URL"</span>]
      <span class="hljs-comment"># Use the application container's IP instead of localhost so Capybara knows where to direct Selenium</span>
      ip = Socket.ip_address_list.detect(&amp;<span class="hljs-symbol">:ipv4_private?</span>).ip_address
      Capybara.app_host = <span class="hljs-string">"http://<span class="hljs-subst">#{ip}</span>:<span class="hljs-subst">#{Capybara.server_port}</span>"</span>
    <span class="hljs-keyword">end</span>

    driven_by <span class="hljs-symbol">:selenium_chrome_custom</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-explanation">Explanation</h3>
<p>Let's look at what each of the code block above is doing.</p>
<h4 id="heading-custom-selenium-chrome-driver">Custom Selenium Chrome driver</h4>
<p><code>Capybara.register_driver :selenium_chrome_custom</code></p>
<p>Since existing Selenium Drivers don't provide the custom options we want, we are creating a new driver <code>selenium_chrome_custom</code> which will handle Remote/Local connection as well as Headless/Headful mode.</p>
<h4 id="heading-options">Options</h4>
<ul>
<li><p><code>--window-size=1400,1400</code></p>
<p>  Set the window size to 1400x1400 pixels. This is a reasonable size without being too large, but you can set it to whatever you like. This mostly impacts the size of debugging screenshots, but some tests may fail if you ask Capybara to click on an element which is not currently visible on the page.</p>
</li>
<li><p><code>--no-sandbox</code></p>
<p>  Disables Chrome’s sandbox functionality because it has an issue with Docker version 1.10.0 and later.</p>
</li>
<li><p><code>--disable-dev-shm-usage</code></p>
<p>  The "/dev/shm" shared memory partition is too small on many VM environments which will cause Chrome to fail or crash so we are disabling it.</p>
</li>
<li><p><code>--headless=new</code></p>
<p>  Enable Chrome’s headless mode which will run Chrome without a UI.</p>
<p>  <code>SELENIUM_HEADFUL</code> will control this option. In development, you may want to run Chrome and see what's happening in the browser; you can do so by running tests with <code>SELENIUM_HEADFUL=true bundle exec rspec spec/system</code>.</p>
<p>  We will see list of other commands to run system tests at the end of this explanation section in a bit.</p>
</li>
</ul>
<p>Some guides may suggest using the --disable-gpu flag, but <a target="_blank" href="https://issues.chromium.org/issues/40527919">this is no longer necessary on any operating system</a>.</p>
<p>This explanation was shamelessly copied from <a target="_blank" href="https://thurlow.io/ruby/2020/11/06/remote-selenium-webdriver-servers-with-rails-capybara-and-rspec.html">Remote Selenium WebDriver servers with Rails, Capybara, RSpec, and Chrome</a> 🙈.</p>
<h4 id="heading-selenium-remote-url">Selenium remote URL</h4>
<p><code>remote_url = ENV["SELENIUM_REMOTE_URL"]</code></p>
<p>Remote option is required mostly for CI but you can also test it out in local by running the Selenium Docker image e.g. with <code>SELENIUM_REMOTE_URL=</code><a target="_blank" href="http://localhost:4444/wd/hub"><code>http://localhost:4444/wd/hub</code></a> <code>bundle exec rspec spec/system</code></p>
<p>Remote option is controlled by <code>SELENIUM_REMOTE_URL</code> which needs to be passed when running tests as seen above.</p>
<p>Another configuration related to the remote is the use of <code>browser: :remote</code> inside <code>Capybara::Selenium::</code><a target="_blank" href="http://Driver.new"><code>Driver.new</code></a> which tells Capybara to run tests in remote Chrome browser instead of local one.</p>
<h4 id="heading-capybara-server-and-app-host">Capybara server and app host</h4>
<pre><code class="lang-ruby">RSpec.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.before(<span class="hljs-symbol">:each</span>, <span class="hljs-symbol">type:</span> <span class="hljs-symbol">:system</span>, <span class="hljs-symbol">js:</span> <span class="hljs-literal">true</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># Make the test app listen to outside requests, required for the remote Selenium instance</span>
    Capybara.server_host = <span class="hljs-string">"0.0.0.0"</span>
    Capybara.server_port = <span class="hljs-number">3000</span>

    <span class="hljs-keyword">if</span> ENV[<span class="hljs-string">"SELENIUM_REMOTE_URL"</span>]
      <span class="hljs-comment"># Use the application container's IP instead of localhost so Capybara knows where to direct Selenium</span>
      ip = Socket.ip_address_list.detect(&amp;<span class="hljs-symbol">:ipv4_private?</span>).ip_address
      Capybara.app_host = <span class="hljs-string">"http://<span class="hljs-subst">#{ip}</span>:<span class="hljs-subst">#{Capybara.server_port}</span>"</span>
    <span class="hljs-keyword">end</span>

    driven_by <span class="hljs-symbol">:selenium_chrome_custom</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><code>server_host</code> and <code>app_host</code> are required for Capybara to know how it can call driver in the Remote Server.</p>
<p>This piece of code was extracted from the <a target="_blank" href="https://guides.rubyonrails.org/testing.html#changing-the-default-settings">official Rails Documentation</a>.</p>
<h4 id="heading-commands-to-run-tests">Commands to run tests</h4>
<p>Lastly, let's see various commands we can use to run system tests.</p>
<ul>
<li><p>Run in headless mode (default): <code>bundle exec rspec spec/system</code></p>
</li>
<li><p>Run in headful mode: <code>SELENIUM_HEADFUL=true bundle exec rspec spec/system</code></p>
</li>
<li><p>Run in headless mode inside external docker image in local: <code>SELENIUM_REMOTE_URL=</code><a target="_blank" href="http://localhost:4444/wd/hub"><code>http://localhost:4444/wd/hub</code></a> <code>bundle exec rspec spec/system</code></p>
</li>
</ul>
<p>For CI, default command <code>bundle exec rspec spec/system</code> will work. But <code>SELENIUM_REMOTE_URL</code> will be <a target="_blank" href="http://selenium:4444/wd/hub"><code>http://selenium:4444/wd/hub</code></a> and it will be passed an Environment Variable instead. We will look at how to do that next.</p>
<h2 id="heading-update-gitlab-ciyml-to-run-all-tests">Update <code>.gitlab-ci.yml</code> to run all tests</h2>
<p>We will be adding code to enable all the following tests and you can choose to pickup or ignore as per your requirement:</p>
<ul>
<li><p>Unit and Integration tests (Model, Requests, Authorization, Services etc.) which don't require us to start browser</p>
</li>
<li><p>System Tests where we will start the Chrome browser and run tests inside it</p>
</li>
</ul>
<p>Update your <code>.gitlab-ci.yml</code> with the configurations given below. Most of the configurations are accompanied by explanation, you can find clean configuration without comment at the end of the blog in the section "Final .gitlab-ci.yml"</p>
<pre><code class="lang-yml"><span class="hljs-comment"># change to the ruby version your application uses</span>
<span class="hljs-attr">image:</span> <span class="hljs-string">ruby:3.3.0</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-attr">MASTER_KEY:</span> <span class="hljs-string">$MASTER_KEY</span>

<span class="hljs-comment"># explanation in next section</span>
<span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn.lock</span>

<span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>

<span class="hljs-comment"># base configuration required for running tests</span>
<span class="hljs-string">.base_db:</span>
  <span class="hljs-comment"># add-on docker images required for running tests</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-comment"># set Rails environment so we don't have to prefix each command with RAILS_ENV=test</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-comment"># Postgres runs in a separate docker image and requires authentication to connect. Disabling that here by using "trust" so it doesn't ask for authentication</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-comment"># use same bundler version that was used in bundling the Gemfile</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">gem</span> <span class="hljs-string">install</span> <span class="hljs-string">bundler</span> <span class="hljs-string">-v</span> <span class="hljs-string">"$(grep -A 1 "</span><span class="hljs-string">BUNDLED</span> <span class="hljs-string">WITH"</span> <span class="hljs-string">Gemfile.lock</span> <span class="hljs-string">|</span> <span class="hljs-string">tail</span> <span class="hljs-string">-n</span> <span class="hljs-number">1</span><span class="hljs-string">)"</span> <span class="hljs-string">--no-document</span>
    <span class="hljs-comment"># install all gems to "vendor" folder which helps in caching of gem installation in between the execution of CI jobs</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">config</span> <span class="hljs-string">set</span> <span class="hljs-string">--local</span> <span class="hljs-string">path</span> <span class="hljs-string">'vendor'</span>
    <span class="hljs-comment"># install "nodejs" required for yarn and "cmake" required for pronto</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">update</span> <span class="hljs-string">-qq</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">install</span> <span class="hljs-string">-y</span> <span class="hljs-string">-qq</span> <span class="hljs-string">nodejs</span> <span class="hljs-string">cmake</span>
    <span class="hljs-comment"># install gems in parallel, nproc returns the number of available processors</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">install</span> <span class="hljs-string">--jobs</span> <span class="hljs-string">$(nproc)</span>
    <span class="hljs-comment"># install yarn</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">curl</span> <span class="hljs-string">-o-</span> <span class="hljs-string">-L</span> <span class="hljs-string">https://yarnpkg.com/install.sh</span> <span class="hljs-string">|</span> <span class="hljs-string">bash</span>
    <span class="hljs-comment"># Make yarn available in the current terminal</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn</span> <span class="hljs-string">install</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cp</span> <span class="hljs-string">config/database.yml.ci</span> <span class="hljs-string">config/database.yml</span>
    <span class="hljs-comment"># 👋 config/application.yml can be different for you. For e.g. if you are using ".env" then this content will be `cat $env &gt; .env`</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cat</span> <span class="hljs-string">$env</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">config/application.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">db:test:prepare</span>

<span class="hljs-attr">unit_and_integration_tests:</span>
  <span class="hljs-comment"># reuse all configurations defined in .base_db above</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-comment"># run this job only when merge requests are created, updated or merged</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-comment"># run all tests except system tests</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rspec</span> <span class="hljs-string">--exclude-pattern</span> <span class="hljs-string">"spec/system/**/*.rb"</span>

<span class="hljs-attr">system_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-comment"># need to declare postgres again because "services" key will override the one defined in .base_db</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
    <span class="hljs-comment"># Docker image for Selenium with Chrome so test can run inside the browser</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">selenium/standalone-chrome:latest</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">selenium</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
    <span class="hljs-comment"># Location of the selenium docker image. "selenium" is an alias, you can also use http://selenium-standalone-chrome:4444/wd/hub or selenium__standalone-chrome (commonly seen in other guides)</span>
    <span class="hljs-attr">SELENIUM_REMOTE_URL:</span> <span class="hljs-string">http://selenium:4444/wd/hub</span>
  <span class="hljs-comment"># store necessary files and folders in case of test failure for debugging the error</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">on_failure</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">log/test.log</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">tmp/screenshots/</span>
    <span class="hljs-attr">expire_in:</span> <span class="hljs-number">1</span> <span class="hljs-string">week</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rspec</span> <span class="hljs-string">spec/system</span>
</code></pre>
<h3 id="heading-explanation-1">Explanation</h3>
<p>Let's look at some configurations where explanation was missing and would be lengthy to add there.</p>
<h4 id="heading-cache">cache</h4>
<pre><code class="lang-yml"><span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn.lock</span>
</code></pre>
<p>This tells Gitlab CI to cache vendor folder where we are storing all our gems, node_modules where all JS packages are stored, yarn.lock which stores the information about installed packages with their versions.</p>
<p>Storing all these folders and files speed up the CI in subsequent runs. <code>bundle install</code> and <code>yarn install</code> will only install new packages that are not already inside the cache.</p>
<h4 id="heading-stages">stages</h4>
<pre><code class="lang-yml"><span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>
</code></pre>
<p>Stages define when to run the jobs. For example, stages that run tests after stages that runs linting on new changes.</p>
<p>If you also have linting and continuous deployment configured then stages could look like this:</p>
<pre><code class="lang-yml"><span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">lint</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging_deploy</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">production_deploy</span>
</code></pre>
<p>Jobs are run in the same order as configured here i.e. linting will run first then test and lastly deployments.</p>
<h4 id="heading-basedb">.base_db</h4>
<p>This configuration is used by all jobs that require database access. All common configurations for such jobs are extracted here.</p>
<p><code>services</code> are add-on docker images and provide capabilities like database, redis, selenium drivers, etc.</p>
<p><code>variables</code> are environment variables used by Rails.</p>
<p><code>before_script</code> runs before the <code>script</code> so anything that needs to be pre-configured can be added here.</p>
<h4 id="heading-unitandintegrationtests">unit_and_integration_tests</h4>
<p><code>extends</code> will extend the configurations defined in the <code>.base_db</code> and use those configurations for this job.</p>
<p><code>stage</code> tells this job at what stage to run. Depending on <code>stages</code> defined just above this job configuration.</p>
<p><code>script</code> are the series of command to execute for this job. We are running all tests except system tests by using the rspec command helper <code>--exclude-pattern "spec/system/**/*.rb</code></p>
<h4 id="heading-systemtests">system_tests</h4>
<p><code>selenium/standalone-chrome:latest</code> configures the docker image for Selenium with Chrome with the latest version. One thing to note is that latest version can be unstable; one time I had to spend 6+ hours in debugging just to find out the latest version "123.0" had some issues and Chrome browser was not starting. You can try with lower versions in that case by adding the version number at the end instead of “latest” e.g. <code>selenium/standalone-chrome:122.0</code> and verify if it resolves the issue.</p>
<p><code>artifacts</code> is used to store necessary files and folders in case of test failure. This helps us in debugging failing tests when needed. We are storing test log files for this purpose.</p>
<h2 id="heading-final-gitlab-ciyml">Final <code>.gitlab-ci.yml</code></h2>
<p>If you also have Pronto or any other linter configured in CI then your final file could look like this:</p>
<pre><code class="lang-yml"><span class="hljs-attr">image:</span> <span class="hljs-string">ruby:3.3.0</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-attr">MASTER_KEY:</span> <span class="hljs-string">$MASTER_KEY</span>

<span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn.lock</span>

<span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>

<span class="hljs-string">.base_db:</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">gem</span> <span class="hljs-string">install</span> <span class="hljs-string">bundler</span> <span class="hljs-string">-v</span> <span class="hljs-string">"$(grep -A 1 "</span><span class="hljs-string">BUNDLED</span> <span class="hljs-string">WITH"</span> <span class="hljs-string">Gemfile.lock</span> <span class="hljs-string">|</span> <span class="hljs-string">tail</span> <span class="hljs-string">-n</span> <span class="hljs-number">1</span><span class="hljs-string">)"</span> <span class="hljs-string">--no-document</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">config</span> <span class="hljs-string">set</span> <span class="hljs-string">--local</span> <span class="hljs-string">path</span> <span class="hljs-string">'vendor'</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">update</span> <span class="hljs-string">-qq</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">install</span> <span class="hljs-string">-y</span> <span class="hljs-string">-qq</span> <span class="hljs-string">nodejs</span> <span class="hljs-string">cmake</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">install</span> <span class="hljs-string">--jobs</span> <span class="hljs-string">$(nproc)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">curl</span> <span class="hljs-string">-o-</span> <span class="hljs-string">-L</span> <span class="hljs-string">https://yarnpkg.com/install.sh</span> <span class="hljs-string">|</span> <span class="hljs-string">bash</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn</span> <span class="hljs-string">install</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cp</span> <span class="hljs-string">config/database.yml.ci</span> <span class="hljs-string">config/database.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cat</span> <span class="hljs-string">$env</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">config/application.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">db:test:prepare</span>

<span class="hljs-attr">unit_and_integration_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rspec</span> <span class="hljs-string">--exclude-pattern</span> <span class="hljs-string">"spec/system/**/*.rb"</span>

<span class="hljs-attr">system_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">selenium/standalone-chrome:latest</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">selenium</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
    <span class="hljs-attr">SELENIUM_REMOTE_URL:</span> <span class="hljs-string">http://selenium:4444/wd/hub</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">on_failure</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">log/test.log</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">tmp/screenshots/</span>
    <span class="hljs-attr">expire_in:</span> <span class="hljs-number">1</span> <span class="hljs-string">week</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rspec</span> <span class="hljs-string">spec/system</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Phew, that was a lot of configurations and explanation. And you can automate all of this with just a single command from Zero Config Rails in near future, stay tuned!</p>
<p>With this, your Rails app now has all type of tests running in the Gitlab CI so you can now merge changes without any worry for them breaking the production application.</p>
<p>Thank you for reading. Happy coding!</p>
<p><strong>References</strong></p>
<ul>
<li><p><a target="_blank" href="https://docs.gitlab.com/ee/ci/services/index.html#how-services-are-linked-to-the-job">How services are linked to the Job (Gitlab)</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/julianrubisch/7a96e4778302c1cb9911b6f9db2cb75f">Gitlab CI Config for System Tests with Minitest (Github Gist)</a></p>
</li>
<li><p><a target="_blank" href="https://thurlow.io/ruby/2020/11/06/remote-selenium-webdriver-servers-with-rails-capybara-and-rspec.html">Remote Selenium WebDriver servers with Rails, Capybara, RSpec, and Chrome</a></p>
</li>
<li><p><a target="_blank" href="https://guides.rubyonrails.org/testing.html#system-testing">System Testing (Official Rails Documentation)</a></p>
</li>
</ul>
<p><strong>Image Credits:</strong></p>
<ul>
<li>Cover Image by <a target="_blank" href="https://unsplash.com/@jenstakesphotos?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Jens Freudenau</a> on <a target="_blank" href="https://unsplash.com/photos/a-group-of-pipes-that-are-connected-to-each-other-Xlg2KbYFUoM?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Configure Minitest with Gitlab CI and Rails]]></title><description><![CDATA[At Zero Config Rails, I (Prabin) am constantly working on automating configurations and boring setups like “Configure Minitest with Gitlab CI”.

💡
You can configure the Gitlab CI for Minitest with a single command using Zero Config Rails. Just hit t...]]></description><link>https://blog.zeroconfigrails.com/configure-minitest-with-gitlab-ci-and-rails</link><guid isPermaLink="true">https://blog.zeroconfigrails.com/configure-minitest-with-gitlab-ci-and-rails</guid><category><![CDATA[Rails]]></category><category><![CDATA[Minitest]]></category><category><![CDATA[Testing]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[GitLab-CI]]></category><dc:creator><![CDATA[Prabin Poudel]]></dc:creator><pubDate>Thu, 26 Sep 2024 08:37:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726816679795/280011a6-a907-4309-8744-04410b897203.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At <a target="_blank" href="https://zeroconfigrails.com">Zero Config Rails</a>, I (<a target="_blank" href="https://x.com/coolprobn">Prabin</a>) am constantly working on automating configurations and boring setups like “Configure Minitest with Gitlab CI”.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can configure the Gitlab CI for Minitest with a single command using Zero Config Rails. Just hit the following command: <code>gem install zcr-zen &amp;&amp; zen add ci:gitlab_ci --app_test_framework=minitest</code></div>
</div>

<p>For the detailed list of configurations, you can visit <a target="_blank" href="https://generators.zeroconfigrails.com/install/gitlab_ci">Gitlab CI Generator</a>.</p>
<p>Now without further ado, let’s jump right into setting up Gitlab CI for Minitest and run those tests in CI.</p>
<h2 id="heading-assumptions">Assumptions</h2>
<ul>
<li><p>You have basic Gitlab CI configurations ready i.e. <code>.gitlab-ci.yml</code> exists in your project.</p>
<p>  If it doesn’t, you can refer to my other article <a target="_blank" href="https://prabinpoudel.com.np/articles/integrate-pronto-with-gitlab-ci-for-rails-app/">Integrate Pronto with Gitlab for Rails App</a></p>
</li>
<li><p>You are using PostgreSQL in your app, though with minimal changes it should work for any other databases.</p>
</li>
<li><p>You are using import-maps, though I have added configurations for projects with esbuild as well, you can just uncomment them.</p>
</li>
</ul>
<h2 id="heading-tested-and-working-in">Tested and working in</h2>
<ul>
<li><p>Ruby 3.3.0</p>
</li>
<li><p>Rails 7.2.1</p>
</li>
<li><p>Minitest 5.25.1</p>
</li>
<li><p>selenium-webdriver 4.24.0</p>
</li>
</ul>
<h2 id="heading-configure-gitlab-ci-variables">Configure Gitlab CI Variables</h2>
<p>Firs of all, we need to add some configurations required by the CI to run tests. This should be done over at <a target="_blank" href="https://gitlab.com">Gitlab</a>.</p>
<h3 id="heading-add-variable-for-storing-environment-variables">Add variable for storing environment variables</h3>
<p>I normally use Figjam which is a maintained version of the popular Figaro gem for storing environment variables which uses <code>config/application.yml</code> but just the plain <code>.env</code> file using dotenv gem is also very popular. Anyway, just copy the content from whatever you are using and paste it inside the Value for this new variable.</p>
<p>You can visit the <a target="_blank" href="https://docs.gitlab.com/ee/ci/variables/#project-cicd-variables">official documentation</a> to learn about setting up variables for Gitlab CI. You have to go to your project's setting in Gitlab and configure these in CI/CD variables.</p>
<p>Create a new variable for storing content in your <code>config/application.yml</code>:</p>
<ol>
<li><p>Type: File</p>
</li>
<li><p>Flags</p>
<p> Uncheck all checklists here i.e. Protect variable, Mask variable and Expand variable reference</p>
</li>
<li><p>Description</p>
<p> You can add “Environment Variables“ but it's optional and you can skip this as "Key" (just below this) is already clear enough on what this variable is storing.</p>
</li>
<li><p>Key: <code>env</code></p>
<p> In "Value", add the copied content from your env file.</p>
<p> <em>NOTE</em>: Make sure to only copy what is under "test" block or ".env.test", you don’t want to add production variables here!</p>
</li>
</ol>
<h3 id="heading-add-variable-for-masterkey">Add variable for <code>MASTER_KEY</code></h3>
<p>Rails comes with <code>config/credentials.yml.enc</code> for storing secrets, we generally also use ENV variables for this but since Rails credentials is the default, we will also look at how to configure those.</p>
<p>To decrypt credentials file, you need MASTER_KEY. If you have generated multiple credentials file per environment then you might have multiple keys like master.key, staging.key, production.key, etc..</p>
<p>Create a new variable for storing content in the “.key” file that can decrypt secrets configured for the test environment; normally this will be inside the <code>config/master.key</code>:</p>
<ol>
<li><p>Type: Variable (Default)</p>
</li>
<li><p>Flags</p>
<p> Uncheck all checklists here i.e. Protect variable, Mask variable and Expand variable reference</p>
</li>
<li><p>Description</p>
<p> Optional. You can leave it blank.</p>
</li>
<li><p>Key: MASTER_KEY</p>
<p> And in Value, add the content copied from the <code>master.key</code></p>
</li>
</ol>
<h2 id="heading-add-databaseymlci-file">Add <code>database.yml.ci</code> file</h2>
<p>It's not considered a good practice to use <code>config/database.yml</code> file for the CI so we will instead create a new file <code>config/database.yml.ci</code> and add configurations required to run tests inside.</p>
<p>You can visit the <a target="_blank" href="https://docs.gitlab.com/ee/ci/variables/#project-cicd-variables">official documentation</a> to learn about setting up variables. You have to go to your project's setting in Gitlab and configure these in CI/CD variables.</p>
<p>After creating the file, add the following:</p>
<pre><code class="lang-yml"><span class="hljs-attr">test:</span>
  <span class="hljs-attr">adapter:</span> <span class="hljs-string">postgresql</span>
  <span class="hljs-attr">encoding:</span> <span class="hljs-string">unicode</span>
  <span class="hljs-attr">host:</span> <span class="hljs-string">postgres</span>
  <span class="hljs-attr">database:</span> <span class="hljs-string">ci_db</span>
  <span class="hljs-attr">username:</span> <span class="hljs-string">postgres</span>
  <span class="hljs-attr">pool:</span> &lt;%=<span class="ruby"> ENV.fetch(<span class="hljs-string">"RAILS_MAX_THREADS"</span>) { <span class="hljs-number">5</span> } </span>%&gt;
</code></pre>
<p>For username it should be “postgres” which is the default user that gets created when postgres service/docker is created hence it doesn’t ask password or tries to authenticate user. You might get an error otherwise because no other users will have been created in postgres at this point: `Please check your database configuration to ensure the username/password are valid`.</p>
<p>For host make sure to use "postgres" instead of “localhost”. For MySQL, you will have to use "mysql" as said in the <a target="_blank" href="https://docs.gitlab.com/ee/ci/services/index.html#how-services-are-linked-to-the-job">official documentation</a>:</p>
<blockquote>
<p>The service container for MySQL is accessible under the hostname mysql. To access your database service, connect to the host named mysql instead of a socket or localhost.</p>
</blockquote>
<p>SQLite doesn’t need any host configurations but other configurations will most probably vary.</p>
<h2 id="heading-configure-capybara-with-selenium">Configure Capybara with Selenium</h2>
<p>We will configure Selenium with Chrome to be used both in CI and Local with Headless mode (by default) while also allowing to run in the browser if needed for debugging.</p>
<p>Create a new file "test/helpers/capybara.rb" and add the following code:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">"selenium-webdriver"</span>

Capybara.register_driver <span class="hljs-symbol">:selenium_chrome_custom</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|app|</span>
  options = Selenium::WebDriver::Chrome::Options.new

  options.add_argument(<span class="hljs-string">"--headless=new"</span>) <span class="hljs-keyword">unless</span> ENV[<span class="hljs-string">"SELENIUM_HEADFUL"</span>]

  options.add_argument(<span class="hljs-string">"--window-size=1400,1400"</span>)
  options.add_argument(<span class="hljs-string">"--no-sandbox"</span>)
  options.add_argument(<span class="hljs-string">"--disable-dev-shm-usage"</span>)

  remote_url = ENV[<span class="hljs-string">"SELENIUM_REMOTE_URL"</span>]

  <span class="hljs-keyword">if</span> remote_url
    Capybara::Selenium::Driver.new(
      app,
      <span class="hljs-symbol">browser:</span> <span class="hljs-symbol">:remote</span>,
      <span class="hljs-symbol">url:</span> remote_url,
      <span class="hljs-symbol">options:</span>
    )
  <span class="hljs-keyword">else</span>
    Capybara::Selenium::Driver.new(app, <span class="hljs-symbol">browser:</span> <span class="hljs-symbol">:chrome</span>, <span class="hljs-symbol">options:</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-explanation">Explanation</h3>
<p>Let's look at what each of the code block above is doing.</p>
<h4 id="heading-custom-selenium-chrome-driver">Custom Selenium Chrome driver</h4>
<p><code>Capybara.register_driver :selenium_chrome_custom</code></p>
<p>Since existing Selenium Drivers don't provide the custom options we want, we are creating a new driver <code>selenium_chrome_custom</code> which will handle Remote/Local connection as well as Headless/Headful mode.</p>
<h4 id="heading-options">Options</h4>
<ul>
<li><p><code>--window-size=1400,1400</code></p>
<p>  Set the window size to 1400x1400 pixels. This is a reasonable size without being too large, but you can set it to whatever you like. This mostly impacts the size of debugging screenshots, but some tests may fail if you ask Capybara to click on an element which is not currently visible on the page.</p>
</li>
<li><p><code>--no-sandbox</code></p>
<p>  Disables Chrome’s sandbox functionality because it has an issue with Docker version 1.10.0 and later.</p>
</li>
<li><p><code>--disable-dev-shm-usage</code></p>
<p>  The "/dev/shm" shared memory partition is too small on many VM environments which will cause Chrome to fail or crash so we are disabling it.</p>
</li>
<li><p><code>--headless=new</code></p>
<p>  Enable Chrome’s headless mode which will run Chrome without a UI.</p>
<p>  <code>SELENIUM_HEADFUL</code> will control this option. In development, you may want to run Chrome and see what's happening in the browser for debugging; you can do so by running tests with <code>SELENIUM_HEADFUL=true bundle exec rails test:system</code>.</p>
<p>  We will see list of other commands to run system tests at the end of this explanation section in a bit.</p>
</li>
</ul>
<p>Some guides may suggest using the --disable-gpu flag, but <a target="_blank" href="https://issues.chromium.org/issues/40527919">this is no longer necessary on any operating system</a>.</p>
<p>This explanation was shamelessly copied from <a target="_blank" href="https://thurlow.io/ruby/2020/11/06/remote-selenium-webdriver-servers-with-rails-capybara-and-rspec.html">Remote Selenium WebDriver servers with Rails, Capybara, RSpec, and Chrome</a> 🙈.</p>
<h4 id="heading-selenium-remote-url">Selenium remote URL</h4>
<p><code>remote_url = ENV[“SELENIUM_REMOTE_URL"]</code></p>
<p>Remote option is required mostly for CI but you can also test it out in local by running the Selenium Docker image e.g. with <code>SELENIUM_REMOTE_URL=</code><a target="_blank" href="http://localhost:4444/wd/hub"><code>http://localhost:4444/wd/hub</code></a> <code>bundle exec rails test:system</code></p>
<p>Remote option is controlled by <code>SELENIUM_REMOTE_URL</code> which needs to be passed when running tests as seen above.</p>
<p>Another configuration related to the remote is the use of <code>browser: :remote</code> inside <code>Capybara::Selenium::</code><a target="_blank" href="http://Driver.new"><code>Driver.new</code></a> which tells Capybara to run tests in remote Chrome browser instead of local one.</p>
<h3 id="heading-add-host-configurations">Add host configurations</h3>
<p>Update the <code>test/application_system_test_case.rb</code> file to include the following content so Gitlab CI can run tests in the remote browser.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># other require declarations ...</span>
<span class="hljs-keyword">require</span> <span class="hljs-string">"helpers/capybara"</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationSystemTestCase</span> &lt; ActionDispatch::SystemTestCase</span>
  <span class="hljs-comment"># other codes ....</span>
  driven_by <span class="hljs-symbol">:selenium_chrome_custom</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setup</span></span>
    Capybara.server_host = <span class="hljs-string">"0.0.0.0"</span> <span class="hljs-comment"># bind to all interfaces</span>
    Capybara.server_port = <span class="hljs-number">3000</span>

    <span class="hljs-keyword">if</span> ENV[<span class="hljs-string">"SELENIUM_REMOTE_URL"</span>].present?
      ip = Socket.ip_address_list.detect(&amp;<span class="hljs-symbol">:ipv4_private?</span>).ip_address
      Capybara.app_host = <span class="hljs-string">"http://<span class="hljs-subst">#{ip}</span>:<span class="hljs-subst">#{Capybara.server_port}</span>"</span>
    <span class="hljs-keyword">end</span>

    <span class="hljs-keyword">super</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Do note, you should replace the line <code>driven_by …</code> with <code>driven_by :selenium_chrome_custom</code> as shown above.</p>
<p><code>server_host</code> and <code>app_host</code> are required for Capybara to know how it can call driver in the Remote Server.</p>
<p>This piece of code was extracted from the <a target="_blank" href="https://guides.rubyonrails.org/testing.html#changing-the-default-settings">official Rails Documentation</a>.</p>
<h3 id="heading-commands-to-run-tests">Commands to run tests</h3>
<p>Lastly, let's see various commands we can use to run system tests.</p>
<ul>
<li><p>Run in headless mode (default): <code>bundle exec rails test:system</code></p>
</li>
<li><p>Run in headful mode: <code>SELENIUM_HEADFUL=true bundle exec rails test:system</code></p>
</li>
<li><p>Run in headless mode inside external docker image in local: <code>SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub bundle exec rails test:system</code></p>
</li>
</ul>
<p>For CI, default command <code>bundle exec rails test:system</code> will work. But <code>SELENIUM_REMOTE_URL</code> will be <a target="_blank" href="http://selenium:4444/wd/hub"><code>http://selenium:4444/wd/hub</code></a> and it will be passed as an Environment Variable instead. We will look at how to do that next.</p>
<h2 id="heading-update-gitlab-ciyml-to-run-all-tests">Update <code>.gitlab-ci.yml</code> to run all tests</h2>
<p>We will be adding code to enable all the following tests and you can choose to pickup or ignore as per your requirement:</p>
<ul>
<li><p>Unit and Integration tests (Model, Requests, Authorization, Services etc.) which don't require us to start browser</p>
</li>
<li><p>System Tests where we will start the Chrome browser and run tests inside it</p>
</li>
</ul>
<p>Update your <code>.gitlab-ci.yml</code> with the configurations given below. Most of the configurations are accompanied by explanation, you can find clean configuration without comment at the end of the blog in the section "<strong>Final</strong> <code>.gitlab-ci.yml</code>"</p>
<pre><code class="lang-yml"><span class="hljs-comment"># change to the ruby version your application uses</span>
<span class="hljs-attr">image:</span> <span class="hljs-string">ruby:3.3.0</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-attr">MASTER_KEY:</span> <span class="hljs-string">$MASTER_KEY</span>

<span class="hljs-comment"># explanation in next section</span>
<span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>
    <span class="hljs-comment"># uncomment till yarn.lock if you are using esbuild i.e. you have package.json in your project</span>
    <span class="hljs-comment"># - node_modules/</span>
    <span class="hljs-comment"># - yarn.lock # or package-lock.json</span>

<span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>

<span class="hljs-comment"># base configuration required for running tests</span>
<span class="hljs-string">.base_db:</span>
  <span class="hljs-comment"># add-on docker images required for running tests</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-comment"># set Rails environment so we don't have to prefix each command with RAILS_ENV=test</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-comment"># Postgres runs in a separate docker image and requires authentication to connect. Disabling that here by using "trust" so it doesn't ask for authentication</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-comment"># use same bundler version that was used in bundling the Gemfile</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">gem</span> <span class="hljs-string">install</span> <span class="hljs-string">bundler</span> <span class="hljs-string">-v</span> <span class="hljs-string">"$(grep -A 1 "</span><span class="hljs-string">BUNDLED</span> <span class="hljs-string">WITH"</span> <span class="hljs-string">Gemfile.lock</span> <span class="hljs-string">|</span> <span class="hljs-string">tail</span> <span class="hljs-string">-n</span> <span class="hljs-number">1</span><span class="hljs-string">)"</span> <span class="hljs-string">--no-document</span>
    <span class="hljs-comment"># install all gems to "vendor" folder which helps in caching of gem installation in between the execution of CI jobs</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">config</span> <span class="hljs-string">set</span> <span class="hljs-string">--local</span> <span class="hljs-string">path</span> <span class="hljs-string">"vendor"</span>
    <span class="hljs-comment"># you can uncomment lines till `yarn install` if you are using esbuild</span>
    <span class="hljs-comment"># - apt-get update -qq</span>
    <span class="hljs-comment"># install "nodejs" required for yarn</span>
    <span class="hljs-comment"># - apt-get install -y -qq nodejs</span>
    <span class="hljs-comment"># - curl -o- -L https://yarnpkg.com/install.sh | bash</span>
    <span class="hljs-comment"># Make yarn available in the current terminal</span>
    <span class="hljs-comment"># - export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"</span>
    <span class="hljs-comment"># - yarn install --pure-lockfile</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">install</span> <span class="hljs-string">--jobs</span> <span class="hljs-string">$(nproc)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cp</span> <span class="hljs-string">config/database.yml.ci</span> <span class="hljs-string">config/database.yml</span>
    <span class="hljs-comment"># config/application.yml can be different for you. If you are using dotenv gem then this content will be `cat $env &gt; .env`</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cat</span> <span class="hljs-string">$env</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">config/application.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">db:test:prepare</span>

<span class="hljs-attr">unit_and_integration_tests:</span>
  <span class="hljs-comment"># reuse all configurations defined in .base_db above</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-comment"># run this job only when merge requests are created, updated or merged</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">test</span>

<span class="hljs-attr">system_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">selenium/standalone-chrome:latest</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">selenium</span>
    <span class="hljs-comment"># need to declare postgres again because "services" key will override the one defined in .base_db</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-comment"># Location of the selenium docker image. "selenium" is an alias, you can also use http://selenium-standalone-chrome:4444/wd/hub or selenium__standalone-chrome (commonly seen in other guides)</span>
    <span class="hljs-attr">SELENIUM_REMOTE_URL:</span> <span class="hljs-string">http://selenium:4444/wd/hub</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">test:system</span>
  <span class="hljs-comment"># store necessary files and folders in case of test failure for debugging the error</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">on_failure</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">log/test.log</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">tmp/screenshots/</span>
    <span class="hljs-attr">expire_in:</span> <span class="hljs-number">1</span> <span class="hljs-string">week</span>
</code></pre>
<h3 id="heading-explanation-1">Explanation</h3>
<p>Let's look at some configurations where explanation was missing and would be lengthy to add there.</p>
<h4 id="heading-cache">cache</h4>
<pre><code class="lang-yml"><span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">node_modules/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">yarn.lock</span>
</code></pre>
<p>This tells Gitlab CI to cache vendor folder where we are storing all our gems, <strong>node_modules</strong> where all JS packages are stored and <strong>yarn.lock</strong> which stores the information about installed packages with their versions.</p>
<p>Storing all these folders and files speed up the CI in subsequent runs. <code>bundle install</code> and <code>yarn install</code> will only install new packages that are not already inside the cache.</p>
<h4 id="heading-stages">stages</h4>
<pre><code class="lang-yml"><span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>
</code></pre>
<p>Stages define when to run the jobs.</p>
<p>If you also have linting and continuous deployment configured then stages could look like this:</p>
<pre><code class="lang-yml"><span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">lint</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging_deploy</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">production_deploy</span>
</code></pre>
<p>Jobs are run in the same order as configured here i.e. linting will run first then test and lastly deployments.</p>
<h4 id="heading-basedb">.base_db</h4>
<p>All common configurations used by jobs that require database access are extracted here.</p>
<p><code>services</code> are add-on docker images and provide capabilities like database, redis, selenium drivers, etc.</p>
<p><code>variables</code> are environment variables used by Rails.</p>
<p><code>before_script</code> runs before the <code>script</code> so anything that needs to be pre-configured can be added here.</p>
<h4 id="heading-unitandintegrationtests">unit_and_integration_tests</h4>
<p><code>extends</code> will extend the configurations defined in the <code>.base_db</code> and use those configurations for this job.</p>
<p><code>stage</code> tells this job at what stage to run. Depending on <code>stages</code> defined just above this job configuration.</p>
<p><code>script</code> are the series of command to execute for running this job.</p>
<h4 id="heading-systemtests">system_tests</h4>
<p><code>selenium/standalone-chrome:latest</code> configures the docker image for Selenium with Chrome with the latest version.</p>
<p><code>artifacts</code> is used to store necessary files and folders in case of test failure. This helps us in debugging failing tests when needed. We are storing test log files for this purpose.</p>
<h2 id="heading-final-gitlab-ciyml">Final <code>.gitlab-ci.yml</code></h2>
<p>This is how your <code>gitlab-ci.yml</code> should look like if you have followed everything in this blog:</p>
<pre><code class="lang-yml"><span class="hljs-attr">image:</span> <span class="hljs-string">ruby:3.3.0</span>

<span class="hljs-attr">variables:</span>
  <span class="hljs-attr">MASTER_KEY:</span> <span class="hljs-string">$MASTER_KEY</span>

<span class="hljs-attr">cache:</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">vendor/</span>

<span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">test</span>

<span class="hljs-string">.base_db:</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">gem</span> <span class="hljs-string">install</span> <span class="hljs-string">bundler</span> <span class="hljs-string">-v</span> <span class="hljs-string">"$(grep -A 1 "</span><span class="hljs-string">BUNDLED</span> <span class="hljs-string">WITH"</span> <span class="hljs-string">Gemfile.lock</span> <span class="hljs-string">|</span> <span class="hljs-string">tail</span> <span class="hljs-string">-n</span> <span class="hljs-number">1</span><span class="hljs-string">)"</span> <span class="hljs-string">--no-document</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">config</span> <span class="hljs-string">set</span> <span class="hljs-string">--local</span> <span class="hljs-string">path</span> <span class="hljs-string">'vendor'</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">install</span> <span class="hljs-string">--jobs</span> <span class="hljs-string">$(nproc)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cp</span> <span class="hljs-string">config/database.yml.ci</span> <span class="hljs-string">config/database.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cat</span> <span class="hljs-string">$env</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">config/application.yml</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">db:test:prepare</span>

<span class="hljs-attr">unit_and_integration_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">test</span>

<span class="hljs-attr">system_tests:</span>
  <span class="hljs-attr">extends:</span> <span class="hljs-string">.base_db</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">test</span>
  <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres:latest</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">selenium/standalone-chrome:latest</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">selenium</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>
    <span class="hljs-attr">POSTGRES_HOST_AUTH_METHOD:</span> <span class="hljs-string">trust</span>
    <span class="hljs-attr">SELENIUM_REMOTE_URL:</span> <span class="hljs-string">http://selenium:4444/wd/hub</span>
  <span class="hljs-attr">only:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">merge_requests</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">test:system</span>
  <span class="hljs-attr">artifacts:</span>
    <span class="hljs-attr">when:</span> <span class="hljs-string">on_failure</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">log/test.log</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">tmp/screenshots/</span>
    <span class="hljs-attr">expire_in:</span> <span class="hljs-number">1</span> <span class="hljs-string">week</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Phew, that was a lot of configurations and explanation. And you can automate all of this with just a single command from Zero Config Rails in near future, stay tuned!</p>
<p>With this, your Rails app now has all type of tests running in the Gitlab CI so you can now merge changes without any worry for them breaking the production application.</p>
<p>Thank you for reading. Happy coding!</p>
<h2 id="heading-references">References</h2>
<ul>
<li><a target="_blank" href="https://prabinpoudel.com.np/articles/setup-and-run-rspec-tests-with-gitlab-ci/">Setup RSpec Tests in Rails with Gitlab CI</a></li>
</ul>
<h2 id="heading-image-credits">Image Credits</h2>
<ul>
<li>Photo by <a target="_blank" href="https://unsplash.com/@ledafenix00?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Tania C</a> on <a target="_blank" href="https://unsplash.com/photos/a-red-heart-shaped-pendant-sitting-on-top-of-a-table-OIohXAQ_lRw?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></li>
</ul>
]]></content:encoded></item></channel></rss>