[RUBY] Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil1-Aktionsimplementierung ohne Authentifizierung-

Einführung

Dieser Artikel wurde geschrieben, weil ich meine Erinnerungen für mich selbst organisieren und organisieren möchte. Natürlich werde ich es so schreiben, dass es nicht peinlich ist, es zu lesen, aber wenn ich es selbst organisieren kann, ist es ein OK-Artikel, daher denke ich, dass es möglicherweise an Erklärungen mangelt, aber es tut mir leid.

Artikelinhalt

――Dieser Artikel wird in Form von TDD entwickelt. Das Testframework verwendet RSpec. ――Wir erstellen ein Artikelverwaltungssystem mit einem einfachen Artikel- und Benutzermodell. --Authentication verwendet die OAuth-Authentifizierung mithilfe der Github-API. --database verwendet das Standard-SQLite3, um sich auf die API zu konzentrieren --Teil1 wird mit dem Ziel fortfahren, die Indexaktion zu implementieren ――Dieser Artikel wird lang sein.

# 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

Vorbereitung auf die Entwicklung

terminal


$ rails new api_name -T --api

-T generiert keine Dateien, die sich auf Minitest beziehen --api kann mit einer Dateistruktur für API beginnen, indem vorab festgelegt wird, dass dieses Projekt nur für API verwendet wird (z. B. wird keine Ansichtsdatei generiert).

gemfile



gem 'rspec-rails'              #rspec gem
gem 'factry_bot_rails'         #Ein Juwel, das Testdaten enthält
gem 'active_model_serializers' #Juwel zum Serialisieren
gem 'kaminari'                 #Edelstein für Pagenat
gem 'octokit', "~> 4.0"        #Juwel für die Arbeit mit Github-Benutzern

Fügen Sie den obigen Edelstein in die Entwicklung ein

bundle install

Damit ist die Vorbereitung für die Entwicklung abgeschlossen

Lassen Sie uns entwickeln

Wenn Sie den Gesamtfluss verfolgen, Implementierung der Indexaktion ↓ Authentifizierungsbezogene Implementierung (Benutzermodellimplementierung) ↓ Implementierung der Erstellungsaktion ↓ Implementierung der Update-Aktion

Ich werde mit dem Fluss fortfahren

Artikel Modell erstellen

$ rails g model title:string content:text slug:string

Erstellen Sie ein Modell mit dieser Konfiguration

Schreiben Sie dann den Testcode in die zuvor erstellte models / article_spec.rb.

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

Der Inhalt dieses Tests besteht darin, dass bei einem leeren Titel eine Fehlermeldung ausgegeben wird und bei der Validierung eine Fehlermeldung abgefangen wird.

$ rspec spec/models/article_spec.rb

Führen Sie den Test aus und führen Sie ihn aus

Überprüfen Sie dann, ob der Fehler angezeigt wird In diesem Fall

undefined method build

Ich erhalte eine solche Fehlermeldung. Dies ist ein Beweis dafür, dass factory_bot nicht funktioniert. Fügen Sie also Folgendes hinzu

ruby:rails_helper.rb:42


config.include FactoryBot::Syntax::Methods

Führen Sie dann rspec erneut aus $ 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

Sie können sehen, dass der Titel als Null gespeichert wurde, ohne von der Validierung erfasst zu werden Beschreiben Sie also die Validierung im Modell

app/models/article.rb


class Article < ApplicationRecord
  validates :title, presence: true
end

Führen Sie den Test erneut aus und bestehen Sie ihn

Und beschreiben Sie dies auf die gleiche Weise für Inhalt und Schnecke

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

Führen Sie den Test aus und bestehen Sie alles

3 examples, 0 failures

Und eine andere Sache, die ich validieren möchte, ist, ob der Slug eindeutig ist

Fügen Sie also einen Test über Unique hinzu

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

Erstellen Sie einen Artikel einmal mit create und erstellen Sie einen Artikel erneut mit build. Und da der Butzen des ersten Artikels für den zweiten Artikel angegeben ist, ist der Butzen der beiden Artikel gleich. Testen Sie damit.

Der Unterschied zwischen Erstellen und Erstellen hängt übrigens davon ab, ob es in der Datenbank gespeichert ist oder nicht, und die Verwendung ändert sich abhängig davon. Wenn Sie create für alles verwenden, ist dies sehr umfangreich. Wenn Sie es also nicht in der Datenbank speichern müssen (bestimmt durch erwartet direkt nach der Erstellung), verwenden Sie build, um den Speicherverbrauch so weit wie möglich zu reduzieren.

Führen Sie den Test aus und sehen Sie den Fehler

failure


expected #<Article id: nil, title: "MyString", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid

Sie können sehen, dass es gespeichert wurde, obwohl es nicht eindeutig ist Also werde ich das Modell beschreiben

app/models/article.rb


validates :slug, uniqueness: true

Löschen Sie dann alle Tests Damit ist der Artikelmodelltest abgeschlossen

articles#index

Als nächstes werden wir den Controller implementieren, zuerst werden wir vom Routing testen

articles#index routing

Erstellen Sie spec / routing / Erstellen Sie dann spec / routing / articles_spec.rb

Beschreiben Sie den Inhalt

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

Dies ist ein Test, um sicherzustellen, dass die Route ordnungsgemäß funktioniert

Ich erhalte eine Fehlermeldung, wenn ich es ausführe

No route matches "/articles"

Ich erhalte die Fehlermeldung, dass das Routing für "/ articles" nicht vorhanden ist Fügen Sie also Routing hinzu

routes.rb


resources :articles, only: [:index]

Test ausführen, aber Fehler erhalten

failure


 A route matches "/articles", but references missing controller: ArticlesController

Dies bedeutet, dass es eine Route namens / article gibt, aber es gibt nichts, was für den Artikel-Controller gilt, also werden wir sie tatsächlich in den Controller schreiben.

$ rails g controller articles

Controller erstellen Beschreiben Sie den Inhalt

app/controllers/articles_controller.rb


def index; end

Der Test geht durch

Übrigens werden wir auch Show-Aktionen implementieren

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

Führen Sie dann den Test aus und stellen Sie sicher, dass er erfolgreich ist.

Artikel # Index Implementierung

Als nächstes werden wir den Inhalt des Controllers tatsächlich implementieren

Zunächst werde ich aus dem Test schreiben

Erstellen Sie eine Datei mit dem Namen spec / controller und erstellen Sie eine Datei mit dem Namen "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

Dieser Test sendet einfach eine get: index-Anforderung und erwartet, dass eine 200-Nummer zurückgegeben wird. : ok hat die gleiche Bedeutung wie 200.

Führen Sie den Test so aus, wie er ist.

expected the response to have status code :ok (200) but it was :no_content (204)

Die Meldung wird angezeigt und der Test schlägt fehl. Diese Nachricht bedeutet, dass ich 200 erwartet habe, aber 204 zurückgegeben wurden. Da 204 nichts zurückgegeben hat, wird es hauptsächlich in Antworten wie Löschen und Aktualisieren verwendet. Aber dieses Mal möchte ich, dass 200 zurückgegeben werden, also werde ich den Controller bearbeiten.

app/controllers/articles_controller.rb


  def index
    articles = Article.all
    render json: articles
  end

Der Inhalt ist einfach, alle Artikel werden aus der Datenbank abgerufen und per Render zurückgegeben. Es wird als "json:" geschrieben, um im json-Format zurückzukehren. Übrigens, selbst wenn Sie "Artikel rendern" so wie es ist zurückgeben, werden 200 zurückgegeben, so dass dieser Test erfolgreich sein wird. Antworten, die nicht im JSON-Format vorliegen, sind nicht vorzuziehen. Später werde ich die Antwort mit dem Serializer analysieren, aber zu diesem Zeitpunkt ist es immer noch notwendig, sie in json zu konvertieren. Vergessen Sie also nicht, json hinzuzufügen :.

Schreiben wir einen Test, um das JSON-Format zu überprüfen

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

Führen Sie diesen Test einmal aus. Ich werde die Erklärung bei Bedarf schreiben.

Validation failed: Slug has already been taken

Zuerst bekomme ich diesen Fehler. Wenn der Slug nicht eindeutig erscheint, wird er validiert. Bearbeiten Sie also den Factory-Bot, um jeden Artikel einzigartig zu machen.

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

Durch die Verwendung der Sequenz auf diese Weise kann der Name jedes Artikels eindeutig gemacht werden.

Lass es uns laufen.

Failure/Error: json_data = json[:data]
no implicit conversion of Symbol into Integer

Ich bekomme eine solche Nachricht und sie schlägt fehl. Dies ist ein Fehler aufgrund des Fehlens von [: data]. Da dies [: data] ist, wenn das Format mit dem Serializer konvertiert wird, wird das JSON-Format mit dem Serializer konvertiert.

Zuerst im Voraus Da der Edelstein in der Beschreibung "gem'active_model_serializers" enthalten ist, wird er mit ihm beschrieben.

Erstellen Sie zunächst eine Datei für den Serializer.

$ rails g serializer article title content slug

Dadurch wird "app / serializers / article_serializer.rb" erstellt.

Und schreiben Sie einen neuen, um den neu eingeführten active_model_serializer anzupassen.

config/initializers/active_model_serializers.rb


ActiveModelSerializers.config.adapter = :json_api

Erstellen Sie diese Datei und fügen Sie die Beschreibung hinzu, um den neu eingeführten Serializer anzupassen.

Auf diese Weise können Sie die Standardeinstellungen ändern und in ein Format konvertieren, das mit [: data] abgerufen werden kann.

Lassen Sie uns den Unterschied zwischen jeder Antwort sehen.

Vor der Einführung von 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"}]

Nach der Installation von 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"}}]}

Sie können sehen, dass sich die Struktur im Inneren geändert hat. Außerdem werden die Attribute "created_at" und "updated_at", die nicht im Serializer angegeben sind, abgeschnitten.

Führen Sie nun den Test erneut aus und er wird erfolgreich sein.

Obwohl es erfolgreich war, gibt es viele doppelte Ausdrücke, daher werde ich es ein wenig umgestalten.

get: index, aber da dies oft mehrmals gesendet wird, definieren Sie alles auf einmal.

describe '#index' do
  subject { get :index }

Durch diese Beschreibung kann es gemeinsam beschrieben werden. Um diese Definition zu verwenden, geben Sie einfach "subject" unter dem definierten Ort ein. Wenn es in der darunter liegenden Hierarchie erneut definiert wird, wird die danach definierte verwendet.

Sie können es verwenden, um das Motiv an zwei Stellen zu ersetzen.

    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

Und dieser Teil kann mit jedem Ausdruck mit_index mit doppelten Ausdrücken kombiniert werden.

Und noch mehr

json = JSON.parse(response.body)
json_data = json['data']

Da diese beiden Beschreibungen häufig wiederholt verwendet werden, werden sie in der Hilfsmethode definiert.

Erstellen Sie "spec / support /" und erstellen Sie "json_api_helpers.rb" darunter.

spec/support/json_api_helpers.rb


module JsonApiHelper
  def json
    JSON.parse(response.body)
  end

  def json_data
    json["data"]
  end
end

Um dies in allen Dateien zu handhaben, fügen Sie es in spec_helper.rb ein.

spec/rails_helper.rb


config.include JsonApiHelpers

Und kommentieren Sie die Beschreibung aus, die den Support zum Lesen gebracht hat.

spec/rails_helper.rb


Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

Auf diese Weise können Sie die vorherige Beschreibung weglassen.

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

Der Index ist fast vollständig. Die letzte Artikelbestellung ist jedoch die letzte. Stellen Sie die Sortierung so ein, dass die neueste Version an erster Stelle steht.

Schreiben Sie zuerst den erwarteten Test.

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

Fügen Sie die obige Beschreibung hinzu. Erstellen Sie einen Artikel zweimal, einen alten und einen neuen. Dann wird durch die Anzahl von json_data bestimmt, ob die neueste zuerst kommt. Die to_s-Methode wird verwendet, da bei Verwendung des Serializers alle Werte in Zeichenfolgen konvertiert werden. Daher müssen die vom Factorybot mithilfe der to_s-Methode generierten Daten in Zeichenfolgen konvertiert werden. Beachten Sie, dass id auch in eine Zeichenfolge konvertiert wird.

Führen Sie nun den Test aus. rspec spec/controllers/articles_controller_spec.rb

failure


expected: "2"
got: "1"

Eine solche Nachricht wird ausgegeben. Ich habe sort noch nicht berührt, also ist es natürlich, aber der letzte Artikel kommt zuerst.

Ich möchte sort implementieren, aber ich möchte es im Modell als Methode beschreiben. Schreiben Sie also zuerst den Modelltest. Der Methodenname lautet .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

Es funktioniert ähnlich wie das Testen eines Controllers, nur dass es vom Controller getrennt ist und sich nur auf den Prozess des Aufrufs der Methode konzentriert. Kurz gesagt, dieser Test ist alles, was Sie tun müssen.

described_class.method_name Auf diese Weise können Sie Klassenmethoden aufrufen. In diesem Fall ist description_class Article, aber was darin enthalten ist, hängt von der zu beschreibenden Datei ab.

Ich möchte auch testen, ob alte Artikel kürzlich beim Aktualisieren erscheinen. Fügen Sie also Folgendes hinzu.

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

Wenn Sie den Test ausführen

Da "undefinierte Methode kürzlich" angezeigt wird, definieren wir "aktuell".

Bisher habe ich geschrieben, dass kürzlich als Methode definiert wurde, aber da der Bereich für diesen Zweck besser geeignet ist, werde ich ihn mit Bereich implementieren. In den meisten Fällen wird der Gültigkeitsbereich genauso behandelt wie die Klassenmethode.

Ich habe einen langen Test geschrieben, aber die Implementierung wird bald beendet sein.

app/models/article.rb


scope :recent, -> { order(created_at: :desc) }

Fügen Sie eine Zeile hinzu, um den Bereich zu definieren.

Alle Tests des Modells, das den Test ausführt, werden grün. rspec spec/models/article_spec.rb

Der Controller fällt jedoch einige Male aus. rspec spec/controllers/articles_controller_spec.rb Das liegt daran, dass ich gerade kürzlich definiert und nicht in controllererd verwendet habe, also werde ich es tatsächlich in controller schreiben.

app/controllers/articles_controller.rb


articles = Article.recent

Ändern Sie Article.all in Article.recent.

Führen Sie nun den Test erneut aus. rspec spec/controllers/articles_controller_spec.rb

Dann schlägt es fehl, weil die Testseite nicht sortieren kann, sodass auch die Testseite sortiert.

spec/controllers/articles_controller_spec.rb


 Article.recent.each_with_index do |article, index| #14. Zeile

Ich habe Article.recent anstelle von articles.recent geschrieben, da Artikel von factory_bot generiert werden und keine tatsächlichen Artikelinstanzen sind. Daher kann .recent nicht verwendet werden.

Da es nicht mehr erforderlich ist, den direkt mit dem Factory-Bot erstellten zu verwenden, articles = create_list :article, 2 Diese Beschreibung ist create_list :article, 2 Erstellen Sie es einfach so.

Führen Sie nun den Test aus und er wird erfolgreich sein.

Implementieren Sie als Nächstes die Paginierung. Mit einer Paginierung können Sie angeben, wie viele Artikel pro Seite vorhanden sind.

Schreiben Sie zunächst wie gewohnt aus dem Test.

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

Fügen Sie diese Zeile hinzu. In params wird ein neuer Parameter angegeben. page gibt die Anzahl der Seiten an und per_page gibt an, wie viele Artikel pro Seite vorhanden sind. In diesem Fall möchte ich eine zweite Seite, und diese Seite enthält nur einen Artikel.

Führen Sie den Test aus.

failure


expected: 1
got: 3

Ich hatte erwartet, dass nur einer zurückgegeben würde, aber alle Artikel wurden zurückgegeben. Also werden wir von hier aus die Paginierung implementieren.

Ich habe bereits am Anfang dieses Artikels einen Edelstein namens Kaminari hinzugefügt. Kaminari ist ein Juwel, das leicht Paginierung realisieren kann.

Also werde ich es mit diesem Juwel implementieren.

app/controllers/articles_controller.rb


    articles = Article.recent.
      page(params[:page]).
      per(params[:per_page])

Nach den letzten werde ich es hinzufügen. Auf diese Weise können Sie .page und .per like oder mapper verwenden. Fügen Sie dann den von params gesendeten Wert in dieses Argument ein.

Damit kann die Antwort eingegrenzt und die von einer Antwort zurückgegebene Datenmenge angegeben werden.

Führen Sie nun den Test erneut aus und er wird erfolgreich sein.

Teil 1, der einst hier geplant war, ist vorbei. Die Implementierung des Index ist abgeschlossen.

Vielen Dank, dass Sie für einen langen Artikel bei uns geblieben sind. Danke für deine harte Arbeit.

Ich habe pert2 gepostet.

Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2

Recommended Posts

Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil1-Aktionsimplementierung ohne Authentifizierung-
Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil3-Aktionsimplementierung mit Authentifizierung
Ich habe versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2 -Benutzerauthentifizierung-
# 8 Seed-Implementierung zum Erstellen einer Bulletin Board-API mit Zertifizierungsautorisierung in Rails 6
Erstellen Sie eine Bulletin Board-API mit Zertifizierung und Autorisierung in Rails 6 # 6. Zeigen Sie, erstellen Sie die Implementierung
Erstellen Sie eine Bulletin Board-API mit Zertifizierung und Autorisierung im Rails 6 # 5-Controller und leiten Sie die Implementierung weiter
Erstellen Sie eine Bulletin Board-API mit Zertifizierungsberechtigung im Rails 6 # 7-Update und zerstören Sie die Implementierung
Verwenden der PAY.JP-API mit Rails ~ Implementierungsvorbereitung ~ (payjp.js v2)
[Schienen] Test mit RSpec
# 4 nach Validierung und Testimplementierung zum Erstellen einer Bulletin Board-API mit Zertifizierung und Autorisierung in Rails 6
Erstellen Sie mit Rails 6 # 18 eine Bulletin-Board-API mit Zertifizierung und Autorisierung. ・ Implementierung des Endbenutzer-Controllers
Erstellen Sie eine Bulletin Board-API mit Zertifizierung und Autorisierung mit Rails 6 # 3 RSpec. FactoryBot wird eingeführt und ein Post-Modell erstellt
[Rails] Ich habe die Validierungsfehlermeldung mit asynchroner Kommunikation implementiert!
[Ruby on Rails] Implementieren Sie die Anmeldefunktion von add_token_to_users mit API
Wovon ich süchtig war, als ich die Google-Authentifizierung mit Rails implementierte