Cet article est écrit parce que je veux organiser et organiser mes souvenirs pour moi-même. Bien sûr, je vais l'écrire pour qu'il ne soit pas gênant de le lire, mais si je peux l'organiser moi-même, c'est un article OK, donc je pense qu'il y a peut-être un manque d'explication, mais je suis désolé.
――Cet article sera développé sous la forme de TDD. Le framework de test utilise RSpec. ――Nous allons créer un système de gestion d'articles qui utilise un modèle d'article et d'utilisateur simple.
# versions
ruby 2.5.1
Rails 6.0.3.1
RSpec 3.9
- rspec-core 3.9.2
- rspec-expectations 3.9.2
- rspec-mocks 3.9.1
- rspec-rails 4.0.1
- rspec-support 3.9.3
terminal
$ rails new api_name -T --api
-T ne génère pas de fichiers liés au minitest --api peut démarrer avec une structure de fichier pour api (par exemple, aucun fichier de vue n'est généré) en pré-déclarant que ce projet sera utilisé uniquement pour l'api
gemfile
gem 'rspec-rails' #gemme rspec
gem 'factry_bot_rails' #Un joyau qui contient des données de test
gem 'active_model_serializers' #Gemme pour sérialiser
gem 'kaminari' #Gemme pour pagenate
gem 'octokit', "~> 4.0" #Gem pour travailler avec les utilisateurs de github
Insérez la gemme ci-dessus dans le développement
bundle install
Ceci termine la préparation au développement
Premièrement, si vous retracez le flux global, Mise en œuvre de l'action d'indexation ↓ Implémentation liée à l'authentification (implémentation du modèle utilisateur) ↓ Mise en œuvre de l'action de création ↓ Mise en œuvre de l'action de mise à jour
Je vais continuer avec le flux
$ rails g model title:string content:text slug:string
Créer un modèle avec cette configuration
Ensuite, écrivez le code de test dans le fichier models / article_spec.rb
créé précédemment.
models/article_spec.rb
require 'rails_helper'
RSpec.describe Article, type: :model do
describe '#validations' do
it 'should validate the presence of the title' do
article = build :article, title: ''
expect(article).not_to be_valid
expect(article.errors.messages[:title]).to include("can't be blank")
end
end
end
Le contenu de ce test est que si le titre est vide, un message d'erreur sera émis, et l'autre est qu'il sera intercepté lors de la validation.
$ rspec spec/models/article_spec.rb
Exécutez et exécutez le test
Vérifiez ensuite que l'erreur apparaît Dans ce cas
undefined method build
J'obtiens une erreur comme celle-ci, c'est la preuve que factory_bot ne fonctionne pas, alors ajoutez ce qui suit
ruby:rails_helper.rb:42
config.include FactoryBot::Syntax::Methods
Puis réexécutez rspec
$ rspec spec/models/article_spec.rb
failure
expected #<Article id: nil, title: "", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid
Vous pouvez voir que le titre a été enregistré comme nul sans être pris en compte lors de la validation Alors, décrivez réellement la validation dans le modèle
app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true
end
Relancez le test et réussissez
Et décrivez-le de la même manière pour le contenu et le slug
models/article_spec.rb
it 'should validate the presence of the content' do
article = build :article, content: ''
expect(article).not_to be_valid
expect(article.errors.messages[:content]).to include("can't be blank")
end
it 'should validate the presence of the slug' do
article = build :article, slug: ''
expect(article).not_to be_valid
expect(article.errors.messages[:slug]).to include("can't be blank")
end
app/models/article.rb
validates :content, presence: true
validates :slug, presence: true
Exécutez le test et passez tout
3 examples, 0 failures
Et une autre chose que je veux appliquer est de savoir si le slug est unique
Alors, ajoutez un test sur unique
models/article_spec.rb
it 'should validate uniqueness of slug' do
article = create :article
invalid_article = build :article, slug: article.slug
expect(invalid_article).not_to be_valid
end
Créez un article une fois avec create et recréez un article avec build. Et puisque le slug du premier article est spécifié pour le deuxième article, le slug des deux articles sera le même. Testez avec ça.
À propos, la différence entre create et build dépend de son enregistrement ou non dans la base de données, et l'utilisation change en fonction de cela. De plus, si vous utilisez create pour tout, cela sera lourd, donc si vous n'avez pas besoin de l'enregistrer dans la base de données (déterminé par expect directement après la création), utilisez build pour réduire autant que possible la consommation de mémoire.
Exécutez le test et voyez l'erreur
failure
expected #<Article id: nil, title: "MyString", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid
Vous pouvez voir qu'il a été enregistré même s'il n'est pas unique en l'état Alors, je vais décrire le modèle
app/models/article.rb
validates :slug, uniqueness: true
Puis effacez tous les tests Ceci termine le test du modèle d'article
articles#index
Ensuite, nous allons implémenter le contrôleur, d'abord nous testerons le routage
articles#index routing
Créer spec / routing /
Créez ensuite spec / routing / articles_spec.rb
Décrivez le contenu
spec/routing/articles_spec.rb
require 'rails_helper'
describe 'article routes' do
it 'should route articles index' do
expect(get '/articles').to route_to('articles#index')
end
end
Ceci est un test pour vous assurer que l'itinéraire fonctionne correctement
J'obtiens une erreur lorsque je l'exécute
No route matches "/articles"
J'obtiens une erreur indiquant que le routage pour / articles
n'existe pas
Alors ajoutez le routage
routes.rb
resources :articles, only: [:index]
Exécutez le test mais obtenez une erreur
failure
A route matches "/articles", but references missing controller: ArticlesController
Cela signifie qu'il existe une route appelée / article, mais que rien ne s'applique au contrôleur d'articles, nous allons donc l'écrire dans le contrôleur.
$ rails g controller articles
Créer un contrôleur Décrivez le contenu
app/controllers/articles_controller.rb
def index; end
Le test passe par
À propos, nous mettrons également en œuvre des actions de spectacle
spec/routing/articles_spec.rb
it 'should route articles show' do
expect(get '/articles/1').to route_to('articles#show', id: '1')
end
routes.rb
resources :articles, only: [:index, :show]
articles_controller.rb
def show
end
Exécutez ensuite le test et assurez-vous qu'il réussit.
Ensuite, nous allons réellement implémenter le contenu du contrôleur
Tout d'abord, j'écrirai à partir du test
Créez un fichier appelé spec / controllers et créez-y un fichier appelé ʻarticles_controller_spec.rb`.
spec/controllers/articles_controller_spec.rb
require 'rails_helper'
describe ArticlesController do
describe '#index' do
it 'should return success response' do
get :index
expect(response).to have_http_status(:ok)
end
end
end
Ce test envoie simplement une requête get: index et s'attend à ce qu'un nombre 200 soit renvoyé. : ok a la même signification que 200.
Exécutez le test tel quel.
expected the response to have status code :ok (200) but it was :no_content (204)
Un message apparaît et le test échoue. Ce message signifie que j'attendais 200 mais 204 a été renvoyé. Puisque 204 n'a rien retourné, il est principalement utilisé dans les réponses telles que la suppression et la mise à jour. Mais cette fois, je veux que 200 soit retourné, donc je vais éditer le contrôleur.
app/controllers/articles_controller.rb
def index
articles = Article.all
render json: articles
end
Le contenu est simple, tous les articles sont extraits de la base de données et renvoyés par render.
Il s'écrit json:
pour retourner au format json.
Au fait, même si vous retournez render articles
tel quel, 200 seront retournés, donc ce test réussira. Les réponses qui ne sont pas au format json ne sont pas préférables. Plus tard, j'analyserai la réponse à l'aide du sérialiseur, mais à ce moment-là, il est encore nécessaire de la convertir en json, alors n'oubliez pas d'ajouter json:.
Écrivons un test pour vérifier le format json
spec/controllers/articles_controller_spec.rb
it 'should return proper json' do
create_list :article, 2
get :index
json = JSON.parse(response.body)
json_data = json['data']
expect(json_data.length).to eq(2)
expect(json_data[0]['attributes']).to eq({
"title" => "My article 1",
"content" => "The content of article 1",
"slug" => "article-1",
})
end
Exécutez ce test une fois. J'écrirai l'explication si nécessaire.
Validation failed: Slug has already been taken
J'obtiens d'abord cette erreur. Si le slug n'apparaît pas unique, il sera validé. Modifiez donc le bot d'usine pour rendre chaque article unique.
spec/factories/articles.rb
FactoryBot.define do
factory :article do
sequence(:title) { |n| "My article #{n}"}
sequence(:content) { |n| "The content of article #{n}"}
sequence(:slug) { |n| "article-#{n}"}
end
end
En utilisant la séquence de cette manière, le nom de chaque article peut être rendu unique.
Lançons-le.
Failure/Error: json_data = json[:data]
no implicit conversion of Symbol into Integer
Je reçois un message comme celui-ci et il échoue. Il s'agit d'une erreur due à l'absence de [: data], et comme ce [: data] correspond au moment où le format est converti à l'aide du sérialiseur, le format json est converti à l'aide du sérialiseur.
D'abord à l'avance
Puisque le gem est inclus dans la description gem'active_model_serializers'
, il sera décrit en l'utilisant.
Tout d'abord, créez un fichier pour le sérialiseur.
$ rails g serializer article title content slug
Cela créera ʻapp / serializers / article_serializer.rb`.
Et écrivez-en un nouveau pour adapter le nouveau active_model_serializer.
config/initializers/active_model_serializers.rb
ActiveModelSerializers.config.adapter = :json_api
Créez ce fichier et ajoutez la description pour adapter le sérialiseur nouvellement introduit.
Cela vous permet de modifier ce qui a été utilisé par défaut et de le convertir dans un format qui peut être récupéré avec [: data].
Voyons la différence entre chaque réponse.
Avant d'introduire ActiveModel :: Serializer
JSON.parse(response.body)
=> [{"id"=>1,
"title"=>"My article 1",
"content"=>"The content of article 1",
"slug"=>"article-1",
"created_at"=>"2020-05-19T06:22:49.045Z",
"updated_at"=>"2020-05-19T06:22:49.045Z"},
{"id"=>2,
"title"=>"My article 2",
"content"=>"The content of article 2",
"slug"=>"article-2",
"created_at"=>"2020-05-19T06:22:49.049Z",
"updated_at"=>"2020-05-19T06:22:49.049Z"}]
Après avoir installé ActiveModel :: Serializer
JSON.parse(response.body)
=> {"data"=>
[{"id"=>"1",
"type"=>"articles",
"attributes"=>
{"title"=>"My article 1", "content"=>"The content of article 1", "slug"=>"article-1"}},
{"id"=>"2",
"type"=>"articles",
"attributes"=>
{"title"=>"My article 2", "content"=>"The content of article 2", "slug"=>"article-2"}}]}
Vous pouvez voir que la structure à l'intérieur a changé. En outre, les attributs created_at et updated_at qui ne sont pas spécifiés dans le sérialiseur sont tronqués.
Maintenant, exécutez à nouveau le test et il réussira.
Bien que cela ait réussi, il existe de nombreuses expressions dupliquées, je vais donc la refactoriser un peu.
get: index
, mais comme il est souvent envoyé plusieurs fois, définissez-le en une seule fois.
describe '#index' do
subject { get :index }
En décrivant de cette manière, il peut être décrit collectivement. Pour utiliser cette définition, tapez simplement «sujet» sous l'emplacement défini. S'il est à nouveau défini dans la hiérarchie en dessous, celui défini après cela sera utilisé.
Vous pouvez l'utiliser pour remplacer le sujet à deux endroits.
it 'should return proper json' do
articles = create_list :article, 2
subject
json = JSON.parse(response.body)
json_data = json['data']
articles.each_with_index do |article, index|
expect(json_data[index]['attributes']).to eq({
"title" => article.title,
"content" => article.content,
"slug" => article.slug,
})
end
Et cette partie peut être combinée avec des expressions dupliquées en utilisant each_with_index.
Et encore plus
json = JSON.parse(response.body)
json_data = json['data']
Étant donné que ces deux descriptions sont souvent utilisées à plusieurs reprises, elles sont définies dans la méthode d'assistance.
Créez spec / support /
et créez json_api_helpers.rb
en dessous.
spec/support/json_api_helpers.rb
module JsonApiHelper
def json
JSON.parse(response.body)
end
def json_data
json["data"]
end
end
Afin de gérer cela dans tous les fichiers, incluez-le dans spec_helper.rb.
spec/rails_helper.rb
config.include JsonApiHelpers
Et décommentez la description qui a fait lire le support.
spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
Cela vous permet d'omettre la description précédente.
spec/controllers/articles_controller_spec.rb
it 'should return proper json' do
articles = create_list :article, 2
subject
articles.each_with_index do |article, index|
expect(json_data[index]['attributes']).to eq({
"title" => article.title,
"content" => article.content,
"slug" => article.slug,
})
end
L'index est presque complet. Cependant, la dernière commande d'articles est la dernière. Définissez le tri de sorte que le dernier vienne en premier.
Tout d'abord, écrivez le test attendu.
spec/controllers/articles_controller_spec.rb
it 'should return articles in the proper order' do
old_article = create :article
newer_article = create :article
subject
expect(json_data.first['id']).to eq(newer_article.id.to_s)
expect(json_data.last['id']).to eq(old_article.id.to_s)
end
Ajoutez la description ci-dessus. Créez un article deux fois, un ancien et un nouveau. Ensuite, il est déterminé si le dernier arrive en premier par le nombre de json_data. La méthode to_s est utilisée car toutes les valeurs sont converties en chaînes lors de l'utilisation du sérialiseur, les données générées par factorybot à l'aide de la méthode to_s doivent donc être converties en chaînes. Notez que id est également converti en chaîne de caractères.
Maintenant, lancez le test.
rspec spec/controllers/articles_controller_spec.rb
failure
expected: "2"
got: "1"
Un tel message est émis. Je n'ai pas encore touché au tri, donc c'est naturel, mais le dernier article arrive en premier.
Je veux implémenter le tri, mais je veux le décrire dans le modèle comme une méthode, alors écrivez d'abord le test du modèle. Le nom de la méthode est .recent.
spec/models/article_spec.rb
describe '.recent' do
it 'should list recent article first' do
old_article = create :article
newer_article = create :article
expect(described_class.recent).to eq(
[ newer_article, old_article ]
)
end
end
Il fait quelque chose de similaire au test d'un contrôleur, sauf qu'il est séparé du contrôleur et se concentre uniquement sur le processus d'appel de la méthode. En bref, ce test est tout ce que vous devez faire.
described_class.method_name
Cela vous permet d'appeler des méthodes de classe.
Dans ce cas, described_class est Article, mais ce qui y est inclus dépend du fichier à décrire.
Je souhaite également tester si d'anciens articles viennent récemment lorsque je mets à jour. Alors ajoutez ce qui suit.
spec/models/article_spec.rb
it 'should list recent article first' do
old_article = create :article
newer_article = create :article
expect(described_class.recent).to eq(
[ newer_article, old_article ]
)
old_article.update_column :created_at, Time.now
expect(described_class.recent).to eq(
[ old_article, newer_article ]
)
end
Lorsque vous exécutez le test
Puisque «méthode non définie récente» apparaît, nous définirons récente.
Jusqu'à présent, j'ai écrit que récent est défini comme une méthode, mais comme la portée est plus adaptée à l'objectif, je vais l'implémenter avec une portée. Dans la plupart des cas, la portée est traitée de la même manière que la méthode de classe.
J'ai écrit un long test, mais l'implémentation sera bientôt terminée.
app/models/article.rb
scope :recent, -> { order(created_at: :desc) }
Ajoutez une ligne pour définir la portée.
Et tous les tests du modèle qui exécute le test deviennent verts.
rspec spec/models/article_spec.rb
Cependant, le contrôleur échoue plusieurs fois.
rspec spec/controllers/articles_controller_spec.rb
C'est parce que je viens de définir récent et que je ne l'ai pas utilisé dans controllerd, donc je l'écrirai réellement dans controller.
app/controllers/articles_controller.rb
articles = Article.recent
Remplacez Article.all par Article.recent.
Maintenant, relancez le test.
rspec spec/controllers/articles_controller_spec.rb
Ensuite, il échoue car le côté test ne peut pas trier, donc le côté test trie également.
spec/controllers/articles_controller_spec.rb
Article.recent.each_with_index do |article, index| #14ème ligne
La raison pour laquelle j'ai décrit Article.recent au lieu de articles.recent est que .recent ne peut pas être utilisé car les articles sont générés par factory_bot et ne sont pas des instances d'article réelles.
Puisqu'il n'est plus nécessaire d'utiliser celui créé directement avec le bot d'usine,
articles = create_list :article, 2
Cette description est
create_list :article, 2
Créez-le simplement comme ça.
Maintenant, lancez le test et il réussira.
Ensuite, implémentez la pagination. Une pagination vous permet de spécifier le nombre d'articles par page.
Tout d'abord, écrivez à partir du test comme d'habitude.
spec/controllers/articles_controller_spec.rb
it 'should paginate results' do
create_list :article, 3
get :index, params: { page: 2, per_page: 1 }
expect(json_data.length).to eq 1
expected_article = Article.recent.second.id.to_s
expect(json_data.first['id']).to eq(expected_article)
end
Ajoutez cette ligne. Un nouveau paramètre est spécifié dans params. page spécifie le nombre de pages et per_page spécifie le nombre d'articles par page. Dans ce cas, je veux une deuxième page, et cette page ne contient qu'un seul article.
Exécutez le test.
failure
expected: 1
got: 3
Je m'attendais à ce qu'un seul soit retourné, mais tous les articles ont été retournés. Nous allons donc implémenter la pagination à partir d'ici.
J'ai déjà ajouté une gemme appelée kaminari au début de cet article. kaminari est un joyau qui peut facilement réaliser la pagination.
Je vais donc l'implémenter en utilisant cette gemme.
app/controllers/articles_controller.rb
articles = Article.recent.
page(params[:page]).
per(params[:per_page])
Suite récente, je vais l'ajouter. De cette façon, vous pouvez utiliser .page et .per like ou mapper. Insérez ensuite la valeur envoyée par params dans cet argument.
Avec cela, la réponse peut être réduite et la quantité de données renvoyée par une réponse peut être spécifiée.
Maintenant, exécutez à nouveau le test et il réussira.
La partie 1 qui était autrefois prévue ici est terminée. L'implémentation de l'index est terminée.
Merci de rester avec nous pour un long article. Je vous remercie pour votre travail acharné.
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2
Recommended Posts