158: Factories, Not Fixtures
Other formats:
בפרק זה נבחן מחדש את נושא יצירת אובייקטי בדיקות (test objects) תוך שימוש במפעלים (factories), ולא בקבועים (fixtures). השימוש בקבועים הוסבר בפרק 60. כיום, ניתן לעשות שימוש במפעלים (factories) דרך מספר כלים שונים הזמינים לנו. בפרק זה נדגים מספר דרכים שונות באמצעותן מפעלים יכולים לשפר את הבדיקות באפליקציית ריילס.
ראשית נבחן מפרט (spec) של מודל User. המפרט כולל שתי בדיקות הקשורות להזדהות (authentication). המבדק הראשון מוודא כי כאשר מעבירים למתודה authenticate שם וסיסמה נכונים מוחזר אובייקט User. המבדק השני מוודא כי כאשר מועבר שם משתמש נכון אך סיסמה שגויה, המתודה מחזירה nil.
require File.dirname(__FILE__) + '/../spec_helper'
describe User do
fixtures :all
it "should authenticate with matching username and password" do
User.authenticate('bob', 'secret').should == users(:bob)
end
it "should not authenticate with incorrect password" do
User.authenticate('bob', 'incorrect').should be_nil
end
end
שימו לב כי כרגע בבדיקות אלה נעשה שימוש בקבועים. לקבועים מספר חולשות ההופכות אותם לפחות מאידיאליים, העיקרית שבהן היא שהם מפרידים את המידע שאנו בודקים מההתנהגות שאנו בודקים: בבדיקה הראשונה למעלה, אנו בודקים את ההתנהגות של מודל User, אך איננו יוצרים User בפועל, אלא מסתמכים על מידע שקיים בקובץ הקבועים (fixtures). הסתמכות זו הופכת בדיקות לשבריריות יותר ומסובכות לקריאה. לעיתים קרובות צריך להסתכל בקובץ הקבועים הנפרד על מנת להבין בדיקה כהלכה, וגם אז – העניינים לא תמיד ברורים.
bob: username: bob email: bob@example.com password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f password_salt: bef65e058905c379436d80d1a32e7374b139e7b0 admin: false admin: username: admin email: admin@example.com password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f password_salt: bef65e058905c379436d80d1a32e7374b139e7b0 admin: true
לדוגמה, היות והסיסמה במודל User מוצפנת, בעוד שבבדיקה עצמה אנו רוצים לבדוק את הסיסמה 'secret', מהסתכלות בקובץ הקבועים לעיל לא ניתן לדעת אם זו הסיסמה הנכונה בכלל.
נפטרים מהקבועים
לפני שאנו מבצעים שינויים כלשהם בבדיקות, עלינו להריץ אותן ולוודא שהן עוברות כולן בהצלחה.
$ rake spec (in /Users/eifion/rails/apps_for_asciicasts/ep158) ..... Finished in 0.217478 seconds 5 examples, 0 failures
הבדיקות עוברות בהצלחה, כך שניתן להתחיל לבצע שינויים. לפני שנתחיל להשתמש במפעלים, ננסה ליצור אובייקטים בצורה ישירה, ונראה כיצד נתקדם. השינוי הראשון שנבצע הוא להיפטר מהתלות בקבועים, וליצור משתמש במסד הנתונים עבור כל בדיקה במפרט.
require File.dirname(__FILE__) + '/../spec_helper'
describe User do
it "should authenticate with matching username and password" do
user = User.create!(:username => "bob", :password => "secret")
User.authenticate('bob', 'secret').should == user
end
it "should not authenticate with incorrect password" do
user = User.create!(:username => "bob", :password => "secret")
User.authenticate('bob', 'incorrect').should be_nil
end
end
כעת נריץ את הבדיקות שנית, ו...…
$ rake spec (in /Users/eifion/rails/apps_for_asciicasts/ep158) ...FF 1) ActiveRecord::RecordInvalid in 'User should authenticate with matching username and password' 2) ActiveRecord::RecordInvalid in 'User should not authenticate with incorrect password' Validation failed: Username has already been taken, Email is invalid Finished in 0.167193 seconds 5 examples, 2 failures
…הפעם קיבלנו שתי שגיאות. מהשגיאה השנייה נראה כי למודל User יש שדה email הכולל תשריר (validation) מסוים אודותיו. נוכל לחזור כעת למפרט ולהוסיף לכל בדיקה ערך כלשהו עבור שדה זה, ואמנם – עבור שתי בדיקות זה לא ייקח זמן רב. אבל אם היו לנו עשרות בדיקות שעושות שימוש במודל User אז היה מדובר בעבודה רבה. אם בשלב כלשהו בעתיד נרצה להוסיף שדה נוסף למודל, אשר כולל תשריר גם הוא, נצטרך לשנות כל אחת ואחת מהבדיקות שיוצרת מופע של המודל. במקרים מסוימים נצטרך להוסיף לבדיקות נתונים עבור שדות שאינם רלוונטיים עבור אותה בדיקה. לדוגמה – עבור שתי הבדיקות שלנו לעיל, השדה email אינו רלוונטי ולא אכפת לנו כלל מה הערך השמור בו (ועדיין, עלינו לספק אחד).
שימוש במפעלים
ניתן לפתור את הבעיה באמצעות שימוש במפעלים. ניתן להשתמש במפעל על מנת ליצור מופע שריר של אובייקט עבור הבדיקות שלנו, ולשנות בכל פעם רק את השדות הרלוונטיים לאותה בדיקה.
ש מספר ג'מים זמינים, ובפרק זה נעשה שימוש ב Factory Girl. על מנת להתקין את Factory Girl, יש להוסיף את השורה הבאה לקובץ /config/environments/test.rb.
config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"
לאחר שהוספנו את השורה, עלינו להריץ את rake על מנת לוודא שהג'ם הותקן.
$ sudo rake gems:install RAILS_ENV=test (in /Users/eifion/rails/apps_for_asciicasts/ep158) gem install thoughtbot-factory_girl --source http://gems.github.com Successfully installed thoughtbot-factory_girl-1.2.1 1 gem installed Installing ri documentation for thoughtbot-factory_girl-1.2.1... Installing RDoc documentation for thoughtbot-factory_girl-1.2.1...
כעת ש-Factory Girl הותקן, עלינו ליצור את המפעל הראשון שלנו. זה תמיד רעיון טוב לשמור את כל המפעלים במקום יחיד, והיות ואנו משתמשים ב-RSpec, ניצור את הקובץ factories.rb תחת התיקייה spec.
כעת, עלינו לגרום לסביבת RSpec להכיר את המפעלים שלנו, וניתן לעשות זאת על ידי הוספת שורה אשר תקשר את factories.rb אל הסביבה. את השורה נוסיף בראש הקובץ /spec/spec_helper.rb.
require File.dirname(__FILE__) + "/factories"
אם היינו עושים שימוש בסביבת בדיקות אחרת, כגון Test::Unit או Shoulda, היינו ממקמים את הקובץ factories.rb ומוסיפים את השורה לעיל ב-/test/test_helper.rb.
משהשלמנו זאת, ניתן ליצור את המפעל הראשון שלנו, זה אשר יטפל במודל User.
Factory.define :user do |f|
f.username "foo"
f.password "foobar"
f.password_confirmation { |u| u.password }
f.email "foo@example.com"
end
אנו מגדירים אובייקט מפעל באמצעות הקריאה Factory.define ומעבירים כפרמטרים את שם המודל, במקרה זה :user, ובלוק אשר קולט את אובייקט המפעל. בתוך הבלוק אנו יכולים להגדיר ערכי ברירת מחדל לשדות והמאפיינים השונים של המודל עבורו אנו מגדירים את המפעל. בקוד לעיל, הוגדרו ארבעה שדות, כאשר לשדות username, password ו-email הוגדרו ערכי מחרוזת, אבל לשדה password_confirmation נעשתה הגדרה שונה. הסיבה לכך היא שאם נקבע את ערך השדה הזה ל “foobar” יהיה עלינו לשנות את שני שדות הסיסמה בכל פעם שנרצה ליצור בבדיקות שלנו אובייקט עם סיסמה שונה מברירת המחדל. במקום זאת, אנו מעבירים בלוק אשר בודק את מצבו הנוכחי של האובייקט ומציב בשדה אישור הסיסמה את אותו הערך בדיוק השמור בשדה הסיסמה. כך נבטיח שהסיסמה ואישור הסיסמה יהיו תמיד תואמים.
כעת שיצרנו מפעל עבור מודל User, אנו יכולים לעדכן את הבדיקות שלנו כדי שיעשו בו שימוש. במקום ליצור אובייקטים ישירות עם הקריאה User.create! ניצור אותם דרך המפעל שלנו.
require File.dirname(__FILE__) + '/../spec_helper'
describe User do
it "should authenticate with matching username and password" do
user = Factory.create(:user, :username => "frank", :password => "secret")
User.authenticate("frank", "secret").should == user
end
it "should not authenticate with incorrect password" do
user = Factory.create(:user, :username => "frank", :password => "secret")
User.authenticate("frank", "incorrect").should be_nil
end
end
בקוד לעיל נעשה כעת שימוש ב-Factory.create על מנת ליצור את המשתמשים שלנו, כאשר הפרמטרים שמועברים הם: סוג האובייקט שאנו רוצים ליצור, ורשימת השדות שאנו רוצים לדרוס את ערכי ברירת המחדל שלהם. שימו לב שעדכנו את שמות המשתמש ל'בוב' ו-'פרנק' כדי שלא תהיה התנגשות עם האובייקטים שמיוצרים על ידי קובץ הקבועים.
אם נריץ את הבדיקות שלנו כעת שוב, הן כולן עוברות בהצלחה.
$ rake spec (in /Users/eifion/rails/apps_for_asciicasts/ep158) ..... Finished in 0.163722 seconds 5 examples, 0 failures
יצירת אובייקטים ברצף
למודל User שלנו יש מספר תשרירים. אובייקטים המיוצרים על ידי המפעל מתמודדים עם רובם היטב, פרט לאחד:
validates_uniqueness_of :username, :email, :allow_blank => true
המודל User שלנו דורש username ייחודי ו-email ייחודי, כך שאנו לא יכולים לכתוב שום בדיקה שמייצרת יותר ממופע אחד, היות והערכים של שדות אלה מקובעים. Factory Girl מספק לנו דרך ליצור מספר אובייקטים ברצף, כך שכל אובייקט מכיל ערכים הייחודיים רק לו.
Factory.define :user do |f|
f.sequence(:username) { |n| "foo#{n}" }
f.password "foobar"
f.password_confirmation { |u| u.password }
f.sequence(:email) { |n| "foo#{n}@example.com" }
end
החלפנו בקוד למעלה את הערכים המקובעים של שם המשתמש וכתובת הדואר האלקטרוני, בקריאה למתודה sequence אליה אנו מעבירים את שם השדה ובלוק. הבלוק מקבל כקלט מספר, שאנו יכולים לעשות בו שימוש כדי לייחד כל ערך שאנו מציבים. עכשיו אנו יכולים ליצור משתמש דרך המפעל שלנו, ויהיה לו username ו-email ייחודיים.
יחסים
בנוסף למודל User, התוכנה שלנו כוללת גם מודל Article. למודל Article יש יחס belongs_to אל המודל User ויש בו תשריר המבטיח כי לכל מאמר יש user_id.
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments, :dependent => :destroy
validates_presence_of :name, :user_id
acts_as_list
def editable_by?(some_user)
some_user.admin? || some_user == user
end
end
Factory Girl מאפשר לנו להגדיר יחסים אלה, באמצעות קריאה למתודה association והעברת שם המודל אליו קיים ייחוס בתור פרמטר.
Factory.define :article do |f| f.name "foo" f.association :user end
כאשר יוצרים אובייקט Article ייעשה חיפוש למציאת מפעל המתאים לייחוס :user ובאופן אוטומטי ייעשה בו שימוש על מנת לבנות אובייקט מתאים. אם לייחוס שלנו שם שונה, לדוגמה author, נוכל להגדיר זאת מפורשות כך:
f.association :author, :factory => :user
עצות וטיפים לסיום
נסיים את הפרק הזה על Factory Girl במבט חוזר לאחת הבדיקות שלנו, ונסתכל על מספר תכונות נוספות הזמינות לנו.
it "should authenticate with matching username and password" do
user = Factory.create(:user, :username => "frank", :password => "secret")
User.authenticate("frank", "secret").should == user
end
כאשר אנו קוראים למתודה Factory.create ליצירת אובייקט, המופע נשמר בפועל במסד הנתונים. אם נרצה לעבוד עם מופע בזיכרון, אך ללא שמירה שלו, נוכל לקרוא במקום זאת למתודה Factory.build.
user = Factory.build(:user, :username => "frank", :password => "secret")
ל-Factory class יש גם מתודה הנקראת attributes_for המחזירה טבלת גיבוב (hash) של ערכי האובייקט.
>> Factory.attributes_for :user
=> {:email=>"foo2@example.com", :password=>"foobar", :username=>"foo2", :password_confirmation=>"foobar"}
מתודה זו שימושית במיוחד לבדיקות של בקרים (controller tests), אשר דורשים טבלת גיבוב כפרמטר לפעולת הבקרים (controller actions). שימו לב לערכים בשדות שם המשתמש וכתובת הדוא"ל, אשר מבוססים על הרצפים עליהם דיברנו קודם לכן.
לסיום, במקום לקרוא ל Factory.create ישירות, נוכל לכתוב בקיצור Factory והתוצאה תהיה זהה.
user = Factory(:user, :username => "frank", :password => "secret")
מתוך יכולותיה של Factory Girl כיסינו רק את הבסיס. למידע נוסף כדאי לעיין בתיעוד.
שווה לקוראים להעיף מבט גם על אלטרנטיבות ל-Factory Girl, בין השאר Machinist3, המאפשר לתאר אובייקטים בתמציתיות רבה.
require 'faker''
Sham.name { Faker::Name.name }
Sham.email { Faker::Internet.email }
Sham.title { Faker::Lorem.sentence }
Sham.body { Faker::Lorem.paragraph }
User.blueprint do
name
email
end
Post.blueprint do
title
author
body
end
אלטרנטיבה אחרת ששווה בדיקה היא Object Daddy, אשר מציג זווית פעולה אחרת, על ידי הוספת המתודה generate לכל מודל ActiveRecord. ואז ניתן לקרוא למתודה מכל מקום בבדיקות, על מנת ליצור מופע שריר של המודל. ניתן להגדיר בתוך המודל עצמו את ערכי ברירת המחדל.
class User < ActiveRecord::Base
generator_for(:start_time) { Time.now }
generator_for :name, 'Joe'
generator_for :age => 25
end
לא משנה מה הפתרון אשר תבחרו, מפעלים הם דרך נפלאה לשפר את הבדיקות באפליקציית הריילס שלכם.

