Continuous Integration

The ability of having immediate feedback on what we are working should be one of the most important characteristics in software development. Imagine making one change to our source code and having to wait 2 weeks to see if it broke something? oh! That would be a nightmare! For this, Continuous Integration will help a team to have immediate and frequent feedback about the status of what they are building.

Martin Fowler defines Continuous Integration as a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly.

In the next subsections, we are going to present two continuous integration tools: GitHub Actions and Circle CI, and use them with a Crystal example application.

These tools not only will let us build and test our code each time the source has changed but also deploy the result (if the build was successful) or use automatic builds, and maybe test against different platforms, to mention a few.

The example application

We are going to use Conway’s Game of Life as the example application. More precisely, we are going to use only the first iterations in Conway’s Game of Life Kata solution using TDD.

Note that we won’t be using TDD in the example itself, but we will mimic as if the example code is the result of the first iterations.

Another important thing to mention is that we are using crystal init to create the application.

And here’s the implementation:

  1. class Location
  2. getter x : Int32
  3. getter y : Int32
  4. def self.random
  5. Location.new(Random.rand(10), Random.rand(10))
  6. end
  7. def initialize(@x, @y)
  8. end
  9. end
  10. class World
  11. @living_cells : Array(Location)
  12. def self.empty
  13. new
  14. end
  15. def initialize(living_cells = [] of Location)
  16. @living_cells = living_cells
  17. end
  18. def set_living_at(a_location)
  19. @living_cells << a_location
  20. end
  21. def is_empty?
  22. @living_cells.size == 0
  23. end
  24. end

And the specs:

  1. require "./spec_helper"
  2. describe "a new world" do
  3. it "should be empty" do
  4. world = World.new
  5. world.is_empty?.should be_true
  6. end
  7. end
  8. describe "an empty world" do
  9. it "should not be empty after adding a cell" do
  10. world = World.empty
  11. world.set_living_at(Location.random)
  12. world.is_empty?.should be_false
  13. end
  14. end

And this is all we need for our continuous integration examples! Let’s start!

Continuous Integration step by step

Here’s the list of items we want to achieve:

  1. Build and run specs using 3 different Crystal’s versions:
    • latest
    • nightly
    • 0.31.1 (using a Docker image)
  2. Install shards packages
  3. Install binary dependencies
  4. Use a database (for example MySQL)
  5. Cache dependencies to make the build run faster

From here choose your next steps: