Zaratan@next

EN

Turtle Family 1

On construit from scratch une app de tortues \o/

Voilà le code produit à la fin de la vidéo: https://github.com/denispasin/turtle_family/tree/end_session_1

Dans cette vidéo, je construit une API REST qui fait un CRUD de tortue. Tout~ est testé (pas forcement en TDD). Il y a eu un petit cafouillage autour de ma database postgres m'obligeant a mettre Docker. J'en reparlerai proprement dans une prochaine vidéo… Vous pouvez aussi aller voir ma vidéo sur le sujet: Docker

Voici un petit résumé de ce qu'on a vu. En espérant que ça vous aide.

Les gems qu'on a utilisé:

Partout

En dev + test

En test

  • factory_bot_rails: Permet de créer facilement des instance de modèle en database.
  • faker: Permet de générer de fausses données de test.
  • nyan-cat-formatter: C'est mignon les nyan cat pendant les tests :)
  • shoulda-matchers: Permet de vérifier les validations facilement dans les tests.

Rappel sur Rspec

Je vais faire un petit résumé sur certains concepts de rspec mais je vous invite a regarder la doc en détail. Cette page notamment: https://relishapp.com/rspec/rspec-expectations/v/3-7/docs/built-in-matchers

Les mots clés

describe/context

Les deux font la même chose et permettent de définir un "contexte" de test. Et donc de séparer nos tests pour les rendre plus lisibles.

On utilisera describe plus pour préciser des noms de classe/méthode:

describe MaClass do
  describe "#new" doend
end

On utilisera context pour décrire un contexte plus global:

describe MaClass do
  context "when the user isn't logged in" doend
end

it

it permet de faire un test. Il est conseillé de ne pas mettre "should" dedans. Par exemple:

it "has 3 elements" doend

on notera l'utilisation de la troisième personne du singulier. Pourquoi fait on cela ? Outre le fait de rendre les tests plus proches de l'anglais, c'est parce qu'après, rspec permet de générer une "doc" de cette façon:

rspec --format doc

Ce qui donne:

TurtlesController
  #create
    creates the turtle
    with no name
      fails
  #index
    returns all the turtles
  #show
    returns the turtle

expect

Ça permet de tester quelque chose. Une liste de matchers peut être trouvé ici: https://relishapp.com/rspec/rspec-expectations/v/3-7/docs/built-in-matchers

expect([1,2,3]).to include(2)

let/let!

let et son ami let! vous permettent de définir une variable qui sera utilisable dans vos tests. Les let sont scope par describe/context.

Un exemple d'utilisation:

context "this will work" do
  let(:ma_var) { "yop" }

  it "checks let" do
    expect(ma_var).to eq("yop")
  end
end

context "this will not work" do
  it "checks let" do
    expect(ma_var).to eq("yop")
  end
end

La différence entre let et let! c'est que let est lazy et ne sera instancié que la première fois que la variable est appelée tandis que let! instancie la variable avant le test. C'est particulièrement pratique quand les let! instancie des objets en database.

before

before permet de définir un block qui sera exécuté avant chaque test. Par exemple:

before do
  Turtle.create(name: 'silvie', color: 'green')
end

it "has one turtle in db" do
  expect(Turtle.count).to eq(1)
end

subject

C'est un peu comme un let ça permet de définir le "sujet" du test. On l'invoque plus tard dans son test en faisant subject

subject do
  get :index
end

it "has an array of turtles" do
  subject
  expect(JSON.parse(response.body)).to be_a(Array)
end

Le fichier .rspec

Le fichier .rspec contient les options par défaut qu'on veut donner a rspec. Dans turtle_family:

--require spec_helper
--require rails_helper
--format NyanCatFormatter

Faire des helpers

Avant le premier helper:

  • On crée le dossier spec/support
  • On ajoute cette ligne dans spec/rails_helper.rb:
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }

Ensuite:

  • On crée un module dans le dossier spec/support. Par exemple:
module JsonHelper
  def json_response
    if (json = JSON.parse(response.body)).is_a?(Array)
      json.map!(&:with_indifferent_access)
    else
      json.with_indifferent_access
    end
  end
end
  • On modifie le spec/rails_helper.rb en ajoutant:
config.include JsonHelper

Après ça toutes les méthode du module sont disponible partout dans les tests. Pour reprendre l'exemple plus haut:

subject do
  get :index
end

it "has an array of turtles" do
  subject
  expect(json_response).to be_a(Array)
end

FactoryBot

FactoryBot est une gem pour nous aider à définir des factories qui permettent de générer des objets en DB dans nos tests.

On l'installe en ajoutant dans spec/rails_helper dans le block de configuration:

config.include FactoryBot::Syntax::Methods

Les factories sont définies dans spec/factories et ressemblent à:

FactoryBot.define do
  factory :turtle do
    name { Faker::OnePiece.character }
    color { %w(blue green pink yellow).sample }
  end
end

On les invoque plus tard dans nos tests de cette façon:

# créer une tortue en db.
create(:turtle)

# créer une tortue en db en spécifiant certains de ses attributs
create(:turtle, name: 'Léonardo')

# créer plusieurs instances (12 ici)
create_list(:turtle, 12)

ShouldaMatchers

Permet de tester les validation des modèles (entre autre).

On l'installe en ajoutant à la fin du spec/rails_helper

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    # Choose a test framework:
    with.test_framework :rspec
    with.library :rails
  end
end

Par exemple si le modèle défini une validation de présence:

validates :name, presence: true

on la test de cette façon:

it { should validate_presence_of(:name) }

La liste des helpers de tests se trouve ici: https://github.com/thoughtbot/shoulda-matchers

Faker

Permet de générer des objets de test. La liste des objets peut être trouvé ici: https://github.com/stympy/faker

Par exemple:

Faker::StarWars.character

ActiveModelSerializer

Permet de ne gérer notre format de retour qu'à un endroit (dans les serializers).

Par défaut le format de retour est un peu simple et ne permet pas de retour des informations de type pagination par exemple.

On la configure en ajoutant un initializer (config/initializers/active_model_serializer.rb) avec:

ActiveModelSerializers.config.adapter = :json # :json_api marche aussi mais est un peu plus complexe a utiliser

on peut ensuite définir des serializer dans app/serializers de cette façon:

class TurtleSerializer < ActiveModel::Serializer
  attributes :id, :name, :color
end

Ensuite, tous nos render json: my_turtle utiliseront par défaut le bon serializer (c'est un objet de classe Turtle donc il va utiliser le TurtleSerializer).

Conclusion

Avec ces outils vous pouvez déjà facilement créer des API simples et plutôt propres. Toutes ces librairies de test vous seront utile dans n'importe quel projet rails/ruby :)