#158 Factories not Fixtures
- Download:
- source codeProject Files in Zip (118 KB)
- mp4Full Size H.264 Video (20 MB)
- m4vSmaller H.264 Video (13.2 MB)
- webmFull Size VP8 Video (36.5 MB)
- ogvFull Size Theora Video (27.1 MB)
בפרק זה נבחן מחדש את נושא יצירת אובייקטי בדיקות (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) <span class="passed">.....</span> Finished in 0.217478 seconds <span class="passed">5 examples, 0 failures</span>
הבדיקות עוברות בהצלחה, כך שניתן להתחיל לבצע שינויים. לפני שנתחיל להשתמש במפעלים, ננסה ליצור אובייקטים בצורה ישירה, ונראה כיצד נתקדם. השינוי הראשון שנבצע הוא להיפטר מהתלות בקבועים, וליצור משתמש במסד הנתונים עבור כל בדיקה במפרט.
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) <span class="passed">...</span><span class="failed">FF</span> 1) <span class="failed">ActiveRecord::RecordInvalid in 'User should authenticate with matching username and password'</span> 2) <span class="failed">ActiveRecord::RecordInvalid in 'User should not authenticate with incorrect password' Validation failed: Username has already been taken, Email is invalid</span> Finished in 0.167193 seconds <span class="failed">5 examples, 2 failures</span>
…הפעם קיבלנו שתי שגיאות. מהשגיאה השנייה נראה כי למודל 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) <span class="passed">.....</span> Finished in 0.163722 seconds <span class="passed">5 examples, 0 failures</span>
יצירת אובייקטים ברצף
למודל 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
לא משנה מה הפתרון אשר תבחרו, מפעלים הם דרך נפלאה לשפר את הבדיקות באפליקציית הריילס שלכם.