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
- active_model_serializers: Permet de gérer ses formats de retour json dans des fichiers de serializers
En dev + test
- rspec-rails: rspec pour rails \o/
- rubocop: rubocop ça check la syntaxe de ton code :)
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" do
…
end
end
On utilisera context
pour décrire un contexte plus global:
describe MaClass do
context "when the user isn't logged in" do
…
end
end
it
it
permet de faire un test. Il est conseillé de ne pas mettre "should" dedans. Par exemple:
it "has 3 elements" do
…
end
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 :)