[RUBY] J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part1-Implémentation de l'action sans authentification-

introduction

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é.

Contenu de l'article

――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

Préparer le développement

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

Développons

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

article Créer un modèle

$ 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.

articles # implémentation d'index

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 posté pert2.

J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2

Recommended Posts

J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part1-Implémentation de l'action sans authentification-
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part3-Implémentation d'action avec authentification-
J'ai essayé d'implémenter l'API Rails avec TDD par RSpec. part2 -authentification de l'utilisateur-
Implémentation n ° 8 pour créer une API de tableau d'affichage avec autorisation de certification dans Rails 6
Créez une API de tableau d'affichage avec certification et autorisation dans Rails 6 # 6 show, créez une implémentation
Construire une API de tableau d'affichage avec certification et autorisation dans le contrôleur Rails 6 # 5, implémentation des routes
Créer une API de tableau d'affichage avec autorisation de certification dans la mise à jour Rails 6 # 7, détruire l'implémentation
Utilisation de l'API PAY.JP avec Rails ~ Préparation de l'implémentation ~ (payjp.js v2)
[Rails] Test avec RSpec
# 4 post-validation et mise en œuvre de test pour créer une API de tableau d'affichage avec certification et autorisation dans Rails 6
Construire une API de tableau d'affichage avec certification et autorisation avec Rails 6 # 18 ・ Implémentation du contrôleur de l'utilisateur final
Créez une API de tableau d'affichage avec certification et autorisation avec Rails 6 # 3 RSpec, FactoryBot introduit et post-modèle
[Rails] J'ai implémenté le message d'erreur de validation avec une communication asynchrone!
[Ruby on Rails] Implémenter la fonction de connexion par add_token_to_users avec l'API
Ce à quoi j'étais accro lors de la mise en œuvre de l'authentification Google avec des rails