في اللغات الديناميكية ، مثل python و javascript ، من الممكن استبدال الطرق والفئات في الوحدات النمطية مباشرة أثناء التشغيل. هذا مناسب جدًا للاختبارات - يمكنك ببساطة وضع "تصحيحات" تستبعد المنطق الثقيل أو غير الضروري في سياق هذا الاختبار.
ولكن ماذا تفعل في C ++؟ تذهب؟ جافا؟ في هذه اللغات ، لا يمكن تعديل الرمز للاختبارات السريعة ، وإنشاء تصحيحات يتطلب أدوات منفصلة.
في مثل هذه الحالات ، يجب عليك كتابة الكود على وجه التحديد حتى يتم اختباره. هذه ليست مجرد رغبة هوس لرؤية تغطية 100 ٪ في مشروعك. هذه خطوة نحو كتابة كود الجودة المدعوم.
سأحاول في هذه المقالة التحدث عن الأفكار الرئيسية وراء كتابة التعليمات البرمجية القابلة للاختبار وإظهار كيف يمكن استخدامها مع مثال لبرنامج الانتقال البسيط.
برنامج غير معقد
سنكتب برنامجًا بسيطًا لتقديم طلب إلى VK API. هذا برنامج بسيط إلى حد ما يقوم بإنشاء طلب ، ويجعله ، ويقرأ الاستجابة ، ويقوم بفك تشفير الاستجابة من JSON في بنية ويعرض النتيجة للمستخدم.
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) const token = "token here" func main() {
كمحترفين في مجالنا ، قررنا أنه كان من الضروري كتابة اختبارات لطلبنا. إنشاء ملف اختبار ...
package main import ( "testing" ) func Test_Main(t *testing.T) { main() }
لا تبدو جذابة للغاية. هذا الاختيار هو إطلاق بسيط لتطبيق لا يمكننا التأثير عليه. لا يمكننا استبعاد العمل مع الشبكة ، والتحقق من إمكانية التشغيل بحثًا عن العديد من الأخطاء ، وحتى استبدال الرمز المميز للتحقق سوف يفشل. دعنا نحاول معرفة كيفية تحسين هذا البرنامج.
نمط حقن التبعية
تحتاج أولاً إلى تنفيذ نمط "حقن التبعية" .
type VKClient struct { Token string } func (client VKClient) ShowUserInfo() { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, )
بإضافة هيكل ، أنشأنا تبعية (مفتاح الوصول) للتطبيق ، والتي يمكن نقلها من مصادر مختلفة ، والتي تتجنب القيم "السلكية" وتبسط الاختبار.
package example import ( "testing" ) const workingToken = "workingToken" func Test_ShowUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
الآن فقط شخص يمكن أن يخطئ ، وبعد ذلك فقط إذا كان يعرف ما يجب أن يكون عليه الاستنتاج. لحل هذه المشكلة ، من الضروري عدم إخراج المعلومات مباشرة إلى دفق الإخراج ، ولكن لإضافة طرق منفصلة للحصول على المعلومات وإخراجها. سيكون هذان الجزءان المستقلان أسهل للتحقق والتحقق.
دعنا ننشئ طريقة GetUserInfo()
، والتي ستُرجع بنية تحتوي على معلومات المستخدم وخطأًا (إذا حدث ذلك). نظرًا لأن هذه الطريقة لا تؤدي إلى إخراج أي شيء ، فسيتم نقل الأخطاء التي تحدث بدون مزيد من المخرجات ، بحيث يكتشف الرمز الذي يحتاج إلى البيانات الموقف.
type UserInfo struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (client VKClient) GetUserInfo() (UserInfo, error) { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) resp, err := http.PostForm(requestURL, nil) if err != nil { return UserInfo{}, err }
تغيير ShowUserInfo()
بحيث يستخدم GetUserInfo()
ويعالج الأخطاء.
func (client VKClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Println(err) return } fmt.Printf( "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
الآن ، في الاختبارات ، يمكنك التحقق من تلقي الإجابة الصحيحة من الخادم ، وإذا كان الرمز غير صحيح ، فسيتم إرجاع خطأ.
func Test_GetUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} userInfo, err := client.GetUserInfo() if err != nil { t.Fatal(err) } if userInfo.ID == 0 { t.Fatal("ID is empty") } if userInfo.FirstName == "" { t.Fatal("FirstName is empty") } if userInfo.LastName == "" { t.Fatal("LastName is empty") } } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but found <nil>") } if err.Error() != "No values in response array" { t.Fatalf(`Expected "No values in response array", but found "%s"`, err) } }
بالإضافة إلى تحديث الاختبارات الحالية ، تحتاج إلى إضافة اختبارات جديدة لطريقة ShowUserInfo()
.
func Test_ShowUserInfo(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_WithError(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
البدائل المخصصة
تشبه اختبارات ShowUserInfo()
ما حاولنا الابتعاد عنه في البداية. في هذه الحالة ، تتمثل النقطة الوحيدة في الطريقة في إخراج المعلومات إلى دفق الإخراج القياسي. من ناحية ، يمكنك محاولة إعادة تعريف os.Stdout والتحقق من المخرجات ، ويبدو أنه حل زائد للغاية عندما يمكنك التصرف بشكل أكثر أناقة.
بدلاً من استخدام fmt.Printf
، يمكنك استخدام fmt.Fprintf
، والذي يسمح لك io.Writer
إلى أي io.Writer
. os.Stdout
بتنفيذ هذه الواجهة ، والتي تسمح لنا باستبدال fmt.Printf(text)
بـ fmt.Fprintf(os.Stdout, text)
. بعد ذلك ، يمكننا وضع os.Stdout
في حقل منفصل ، والذي يمكن تعيينه على القيم المطلوبة (للاختبارات - سلسلة ، للعمل - دفق إخراج قياسي).
نظرًا لأن القدرة على تغيير Writer للناتج نادرًا ما تستخدم ، خاصة للاختبارات ، فمن المنطقي تعيين قيمة افتراضية. نذهب ، لهذا سنقوم بذلك - جعل VKClient
نوع VKClient
للتصدير وإنشاء وظيفة منشئ لذلك.
type vkClient struct { Token string OutputWriter io.Writer } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, } }
في وظيفة ShowUserInfo()
، استبدلنا المكالمات Print
بـ Fprintf
.
func (client vkClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Fprintf(client.OutputWriter, err.Error()) return } fmt.Fprintf( client.OutputWriter, "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
تحتاج الآن إلى تحديث الاختبارات بحيث تقوم بإنشاء العميل باستخدام المنشئ وتثبيت كاتب آخر عند الضرورة.
func Test_ShowUserInfo(t *testing.T) { client := CreateVKClient(workingToken) buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) matched, err := regexp.Match( `Your id: \d+\nYour full name: [^\n]+\n`, result, ) if err != nil { t.Fatal(err) } if !matched { t.Fatalf(`Expected match but failed with "%s"`, result) } } func Test_ShowUserInfo_WithError(t *testing.T) { client := CreateVKClient("") buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) if string(result) != "No values in response array" { t.Fatal("Wrong error") } }
لكل اختبار يتم فيه إخراج شيء ما ، نقوم بإنشاء مخزن مؤقت يلعب دور دفق الإخراج القياسي. بعد تنفيذ الوظيفة ، يتم التحقق من أن النتائج تتوافق مع توقعاتنا - باستخدام تعبيرات منتظمة أو مقارنة بسيطة.
لماذا أستخدم التعبيرات العادية؟ لكي تعمل الاختبارات مع أي رمز مميز سقدمه للبرنامج ، بغض النظر عن اسم المستخدم ومعرف المستخدم.
نمط حقن التبعية - 2
في الوقت الحالي ، يتمتع البرنامج بتغطية قدرها 86.4 ٪. لماذا لا 100 ٪؟ لا يمكننا استفزاز الأخطاء من http.PostForm()
و ioutil.ReadAll()
و json.Unmarshal()
، مما يعني أنه لا يمكننا التحقق من كل " return UserInfo, err
".
من أجل منح نفسك مزيدًا من التحكم في الموقف ، تحتاج إلى إنشاء واجهة يمكن بموجبها http.Client
، والتي سيكون تنفيذها في vkClient ، ويستخدم لعمليات الشبكة. بالنسبة لنا ، في الواجهة ، هناك طريقة واحدة فقط PostForm
- PostForm
.
type Networker interface { PostForm(string, url.Values) (*http.Response, error) } type vkClient struct { Token string OutputWriter io.Writer Networker Networker } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, &http.Client{}, } }
هذه الخطوة تلغي الحاجة إلى إجراء عمليات الشبكة بشكل عام. الآن يمكننا ببساطة إرجاع البيانات المتوقعة من فكونتاكتي باستخدام Networker
وهمية. بالطبع ، لا تتخلص من الاختبارات التي ستقوم بفحص الطلبات على الخادم ، ولكن ليست هناك حاجة لتقديم طلبات في كل اختبار.
سنقوم بإنشاء تطبيقات لـ Networker
و Reader
المزيفة ، حتى نتمكن من اختبار الأخطاء في كل حالة - عند الطلب ، عند قراءة النص وأثناء إلغاء التسلسل. إذا كنا نريد خطأً عند الاتصال بـ PostForm ، فإننا ببساطة نعيده بهذه الطريقة. إذا كنا نريد خطأ
عند قراءة نص الاستجابة - تحتاج إلى إرجاع Reader
مزيف ، والذي سوف يلقي خطأً. وإذا كنا بحاجة إلى الخطأ لإظهار نفسه أثناء إلغاء التسلسل ، فإننا نرجع الإجابة بسلسلة فارغة في النص. إذا كنا لا نريد أي أخطاء ، فنحن ببساطة نعيد النص بالمحتويات المحددة.
type fakeReader struct{} func (fakeReader) Read(p []byte) (n int, err error) { return 0, errors.New("Error on read") } type fakeNetworker struct { ErrorOnPostForm bool ErrorOnBodyRead bool ErrorOnUnmarchal bool RawBody string } func (fn *fakeNetworker) PostForm(string, url.Values) (*http.Response, error) { if fn.ErrorOnPostForm { return nil, fmt.Errorf("Error on PostForm") } if fn.ErrorOnBodyRead { return &http.Response{Body: ioutil.NopCloser(fakeReader{})}, nil } if fn.ErrorOnUnmarchal { fakeBody := ioutil.NopCloser(bytes.NewBufferString("")) return &http.Response{Body: fakeBody}, nil } fakeBody := ioutil.NopCloser(bytes.NewBufferString(fn.RawBody)) return &http.Response{Body: fakeBody}, nil }
لكل حالة مشكلة ، نضيف اختبار. سيقومون بإنشاء Networker
مزيفة بالإعدادات اللازمة ، والتي بموجبها سيقوم برمي خطأ في نقطة معينة. بعد ذلك ، ندعو إلى التحقق من الوظيفة والتأكد من حدوث خطأ ، وتوقعنا حدوث هذا الخطأ.
func Test_GetUserInfo_ErrorOnPostForm(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnPostForm: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on PostForm" { t.Fatalf(`Expected "Error on PostForm" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnBodyRead(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnBodyRead: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on read" { t.Fatalf(`Expected "Error on read" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnUnmarchal(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnUnmarchal: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } const expectedError = "unexpected end of JSON input" if err.Error() != expectedError { t.Fatalf(`Expected "%s" but got "%s"`, expectedError, err.Error()) } }
باستخدام حقل RawBody
، RawBody
التخلص من طلبات الشبكة (ما RawBody
سوى إرجاع ما نتوقع أن نتلقاه من VKontakte). قد يكون هذا ضروريًا لتجنب تجاوز حدود الاستعلام أثناء الاختبار أو لتسريع الاختبارات.
النتائج
بعد كل العمليات في المشروع ، تلقينا حزمة من 91 سطرًا (+170 سطرًا من الاختبارات) ، والتي تدعم الإخراج إلى أي io.Writer
، تتيح لك استخدام طرق بديلة للعمل مع الشبكة (باستخدام المحول إلى الواجهة الخاصة بنا) ، حيث توجد طريقة مثل لإخراج البيانات ، والحصول عليها. المشروع لديه تغطية 100 ٪. اختبارات تحقق بالكامل من كل سطر واستجابة التطبيق لكل خطأ ممكن.
كل خطوة على الطريق إلى تغطية 100 ٪ زادت من نمطية ، وسهولة الصيانة وموثوقية التطبيق ، لذلك لا يوجد شيء خاطئ مع حقيقة أن الاختبارات تملي هيكل الحزمة.
اختبار أي رمز هو جودة لا تظهر من الجو. تظهر قابلية الاختبار عندما يستخدم المطور أنماطًا كافية في المواقف المناسبة ويكتب رمزًا مخصصًا ونموذجيًا. كانت المهمة الرئيسية هي إظهار عملية التفكير عند تنفيذ برامج إعادة الإسكان. يمكن أن يمتد التفكير المماثل إلى أي تطبيق ومكتبة ، وكذلك اللغات الأخرى.