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.
――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
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
Wenn Sie den Gesamtfluss verfolgen, Implementierung der Indexaktion ↓ Authentifizierungsbezogene Implementierung (Benutzermodellimplementierung) ↓ Implementierung der Erstellungsaktion ↓ Implementierung der Update-Aktion
Ich werde mit dem Fluss fortfahren
$ 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.
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 versucht, die Rails-API mit TDD von RSpec zu implementieren. Teil2
Recommended Posts