
ربما ، يوجد في مكان ما مقال مثالي يكشف على الفور وبشكل كامل عن موضوع هندسة الاختبار ، ويسهل كتابته وقراءته ودعمه ، وبذلك يكون مفهومًا للمبتدئين ، مع أمثلة على التنفيذ ومجالات التطبيق. أود أن أقدم رؤيتي لهذه "المقالة المثالية" بالصيغة التي حلمت بها ، فقط بعد تلقي المهمة الأولى "كتابة الاختبارات التلقائية". للقيام بذلك ، سأتحدث عن الأساليب المعروفة وغير المعروفة جيدًا للاختبارات التلقائية على الويب ، ولماذا وكيف ومتى يتم استخدامها ، بالإضافة إلى الحلول الناجحة لتخزين البيانات وإنشائها.
مرحبا هبر! اسمي ديانا ، أنا رئيس مجموعة اختبار واجهة المستخدم ، لقد كنت أتمتة اختبارات الويب وسطح المكتب لمدة خمس سنوات. ستكون أمثلة الكود في جافا وعلى الويب ، ولكن في الممارسة العملية ، تم اختبارها ، والأساليب قابلة للتطبيق على Python مع سطح المكتب.
في البداية كان ...
في البداية كانت هناك كلمة ، وكان هناك الكثير من الكلمات ، وقد ملأت جميع الصفحات بالتساوي مع التعليمات البرمجية ، بغض النظر عن البنيات الخاصة بك ومبادئ DRY (لا تكرر نفسك - لا داعي لتكرار الكود الذي كتبته بالفعل ثلاث فقرات أعلاه).
ورقة
في الواقع ، فإن بنية "footcloth" ، المعروفة أيضًا باسم "sheet" ، والتي تُعرف أيضًا باسم "الشفرة غير المنظمة" المتراكمة في كومة تملأ الشاشة بشكل متساوٍ ، ليست سيئة للغاية ويمكن تطبيقها تمامًا في المواقف التالية:
- نقرة سريعة في ثلاثة أسطر (حسنًا ، مائتان وثلاثة) للمشاريع الصغيرة جدًا ؛
- للحصول على أمثلة التعليمات البرمجية في العرض التوضيحي المصغر ؛
- للكود الأول في نمط "Hello Word" بين الاختبارات التلقائية.
ماذا عليك أن تفعل للحصول على بنية ملاءة السرير؟ فقط اكتب كل الكود اللازم في ملف واحد ، لوحة قماشية مشتركة.
import com.codeborne.selenide.Condition;
import com.codeborne.selenide.WebDriverRunner;
import org.testng.annotations.Test;
import static com.codeborne.selenide.Selenide.*;
public class RandomSheetTests {
@Test
void addUser() {
open("https://ui-app-for-autotest.herokuapp.com/");
$("#loginEmail").sendKeys("test@protei.ru");
$("#loginPassword").sendKeys("test");
$("#authButton").click();
$("#menuMain").shouldBe(Condition.appear);
$("#menuUsersOpener").hover();
$("#menuUserAdd").click();
$("#dataEmail").sendKeys("mail@mail.ru");
$("#dataPassword").sendKeys("testPassword");
$("#dataName").sendKeys("testUser");
$("#dataGender").selectOptionContainingText("");
$("#dataSelect12").click();
$("#dataSelect21").click();
$("#dataSelect22").click();
$("#dataSend").click();
$(".uk-modal-body").shouldHave(Condition.text(" ."));
WebDriverRunner.closeWebDriver();
}
}
إذا كنت قد بدأت للتو في التعرف على الاختبارات التلقائية ، فإن "الورقة" كافية بالفعل لإكمال مهمة اختبار بسيطة ، خاصة إذا أظهرت معرفة جيدة بتصميم الاختبار والتغطية الجيدة. لكن هذا سهل للغاية بالنسبة للمشاريع الكبيرة ، لذلك إذا كانت لديك طموحات ، ولكن ليس لديك وقت لتنفيذ كل حالة اختبار بشكل مثالي ، فيجب على الأقل أن يكون لديك مثال على بنية أكثر تعقيدًا.
PageObject
هل سمعت عن شائعات مفادها أن PageObject تم إهمالها؟ أنت فقط لا تعرف كيف تطبخه!
وحدة العمل الرئيسية في هذا النمط هي "صفحة" ، أي مجموعة كاملة من العناصر والإجراءات معها ، على سبيل المثال ، MenuPage - فئة تصف جميع الإجراءات بقائمة ، أي النقرات على علامات التبويب ، وتوسيع العناصر المنسدلة ، وما إلى ذلك.
من الأصعب قليلاً تكوين كائن PageObject للنافذة المشروطة (اختصارًا "مشروط") لإنشاء الكائن. مجموعة حقول الفصل واضحة: جميع حقول الإدخال ، ومربعات الاختيار ، والقوائم المنسدلة ؛ وللأساليب ، هناك خياران: يمكنك جعل كلا الطريقتين العامتين "ملء جميع الحقول المشروطة" ، و "ملء جميع الحقول المشروطة بقيم عشوائية" ، و "التحقق من جميع حقول modalk" ، والطرق المنفصلة "ملء الاسم" ، و "التحقق من الاسم" ، "املأ الوصف" وما إلى ذلك. يتم تحديد ما يجب استخدامه في حالة معينة حسب الأولويات - تزيد طريقة "طريقة واحدة للنهج النموذجي بالكامل" من سرعة كتابة الاختبار ، ولكنها تفقد الكثير في قابلية قراءة الاختبار مقارنة بنهج "طريقة واحدة لكل مجال".
مثال
دعنا نؤلف كائن صفحة مشتركًا لإنشاء مستخدمين لكلا النوعين من الاختبارات:
:
:
. : , , , , — . , , , .
public class UsersPage {
@FindBy(how = How.ID, using = "dataEmail")
private SelenideElement email;
@FindBy(how = How.ID, using = "dataPassword")
private SelenideElement password;
@FindBy(how = How.ID, using = "dataName")
private SelenideElement name;
@FindBy(how = How.ID, using = "dataGender")
private SelenideElement gender;
@FindBy(how = How.ID, using = "dataSelect11")
private SelenideElement var11;
@FindBy(how = How.ID, using = "dataSelect12")
private SelenideElement var12;
@FindBy(how = How.ID, using = "dataSelect21")
private SelenideElement var21;
@FindBy(how = How.ID, using = "dataSelect22")
private SelenideElement var22;
@FindBy(how = How.ID, using = "dataSelect23")
private SelenideElement var23;
@FindBy(how = How.ID, using = "dataSend")
private SelenideElement save;
@Step("Complex add user")
public UsersPage complexAddUser(String userMail, String userPassword, String userName, String userGender,
boolean v11, boolean v12, boolean v21, boolean v22, boolean v23) {
email.sendKeys(userMail);
password.sendKeys(userPassword);
name.sendKeys(userName);
gender.selectOption(userGender);
set(var11, v11);
set(var12, v12);
set(var21, v21);
set(var22, v22);
set(var23, v23);
save.click();
return this;
}
@Step("Fill user Email")
public UsersPage sendKeysEmail(String text) {...}
@Step("Fill user Password")
public UsersPage sendKeysPassword(String text) {...}
@Step("Fill user Name")
public UsersPage sendKeysName(String text) {...}
@Step("Select user Gender")
public UsersPage selectGender(String text) {...}
@Step("Select user variant 1.1")
public UsersPage selectVar11(boolean flag) {...}
@Step("Select user variant 1.2")
public UsersPage selectVar12(boolean flag) {...}
@Step("Select user variant 2.1")
public UsersPage selectVar21(boolean flag) {...}
@Step("Select user variant 2.2")
public UsersPage selectVar22(boolean flag) {...}
@Step("Select user variant 2.3")
public UsersPage selectVar23(boolean flag) {...}
@Step("Click save")
public UsersPage clickSave() {...}
private void set(SelenideElement checkbox, boolean flag) {
if (flag) {
if (!checkbox.isSelected()) checkbox.click();
} else {
if (checkbox.isSelected()) checkbox.click();
}
}
}
:
@Test
void addUser() {
baseRouter.authPage()
.complexLogin("test@protei.ru", "test")
.complexOpenAddUser()
.complexAddUser("mail@test.ru", "pswrd", "TESTNAME", "", true, false, true, true, true)
.checkAndCloseSuccessfulAlert();
}
:
@Test
void addUserWithoutComplex() {
//Arrange
baseRouter.authPage()
.complexLogin("test@protei.ru", "test");
//Act
baseRouter.mainPage()
.hoverUsersOpener()
.clickAddUserMenu();
baseRouter.usersPage()
.sendKeysEmail("mail@test.ru")
.sendKeysPassword("pswrd")
.sendKeysName("TESTNAME")
.selectGender("")
.selectVar11(true)
.selectVar12(false)
.selectVar21(true)
.selectVar22(true)
.selectVar23(true)
.clickSave();
//Assert
baseRouter.usersPage()
.checkTextSavePopup(" .")
.closeSavePopup();
}
. : , , , , — . , , , .
خلاصة القول هي أنه داخل الصفحات يتم تغليف جميع الإجراءات مع الصفحات (التنفيذ مخفي ، تتوفر الإجراءات المنطقية فقط) ، وبالتالي ، يستخدم الاختبار وظائف العمل. وهذا بدوره يسمح لك بكتابة صفحاتك الخاصة لكل منصة (الويب ، سطح المكتب ، الهواتف المحمولة) ، دون تغيير الاختبارات.
المؤسف الوحيد هو أن الواجهات المتطابقة تمامًا نادرة على منصات مختلفة.
لتقليل التناقض بين الواجهات ، هناك إغراء لتعقيد الخطوات الفردية ، ويتم إخراجها إلى فئات وسيطة منفصلة ، وتصبح الاختبارات أقل وأقل قابلية للقراءة ، حتى خطوتين: "تسجيل الدخول ، قم بعمل جيد" ، انتهى الاختبار. بالإضافة إلى الويب ، لم تكن هناك واجهات إضافية في مشاريعنا ، وعلينا قراءة الحالات أكثر من الكتابة ، لذلك ، من أجل سهولة القراءة ، اكتسبت PageObjects التاريخية مظهرًا جديدًا.
PageObject هي لعبة كلاسيكية يعرفها الجميع. يمكنك العثور على العديد من المقالات حول هذا النهج مع أمثلة في أي لغة برمجة تقريبًا. غالبًا ما يستخدم استخدام PageObject للحكم على ما إذا كان المرشح يعرف شيئًا عن اختبار واجهات المستخدم. إن أداء مهمة اختبار باستخدام هذا النهج هو ما يتوقعه معظم أصحاب العمل ، ويعيش الكثير منه في مشاريع الإنتاج ، حتى لو كان الويب هو الوحيد الذي يختبر.
ماذا يحدث بعد ذلك؟
الغريب ، لا يوجد كائن PageObject واحد!
- غالبًا ما يتم العثور على نمط ScreenPlay ، والذي يمكنك القراءة عنه ، على سبيل المثال ، هنا . لم يتجذر في بلدنا ، لأن استخدام نهج bdd دون إشراك الأشخاص الذين لا يستطيعون قراءة الكود هو عنف لا معنى له ضد الأوتوماتيكيين.
- js- , PageObject, - , , .
- - , , ModelBaseTesting, . , .
وسأخبرك بمزيد من التفاصيل حول عنصر الصفحة ، والذي يسمح لك بتقليل كمية نفس النوع من التعليمات البرمجية ، مع زيادة قابلية القراءة وتوفير فهم سريع للاختبارات حتى لأولئك الذين ليسوا على دراية بالمشروع. وعليها (مع البلاك جاك والتفضيلات الخاصة بها ، بالطبع!) تم بناء الأطر الشائعة التي لا تنتمي إلى js htmlElements و Atlas و Epam's JDI.
ما هو عنصر الصفحة؟
لإنشاء نمط Page Element ، فلنبدأ بالعنصر ذي المستوى الأدنى. كما تقول ويكتيوناري ، فإن "عنصر واجهة المستخدم" هو برنامج بدائي لواجهة مستخدم رسومية ذات مظهر قياسي وتنفذ إجراءات قياسية. على سبيل المثال ، أبسط أداة "زر" - يمكنك النقر عليها ، يمكنك التحقق من نصها ولونها. في "حقل الإدخال" ، يمكنك إدخال نص ، والتحقق من النص الذي تم إدخاله ، والنقر ، والتحقق من عرض التركيز ، والتحقق من عدد الأحرف التي تم إدخالها ، وإدخال النص والضغط على "إدخال" ، والتحقق من العنصر النائب ، والتحقق من تمييز الحقل "إلزامي" ونص الخطأ ، وهذا كل شيء ، ما الذي قد يكون مطلوبًا في حالة معينة. علاوة على ذلك ، فإن جميع الإجراءات مع هذا الحقل قياسية في أي صفحة.
هناك عناصر واجهة تعامل أكثر تعقيدًا لا تكون الإجراءات فيها واضحة جدًا ، على سبيل المثال ، جداول المحتويات المتفرعة. عند كتابتها ، تحتاج إلى البناء على ما يفعله المستخدم في هذا المجال من البرنامج ، على سبيل المثال:
- انقر فوق عنصر في جدول المحتويات بالنص المحدد ،
- التحقق من وجود عنصر بالنص المحدد ،
- التحقق من المسافة البادئة لعنصر بالنص المحدد.
يمكن أن تكون الأدوات من نوعين: مع محدد موقع في المُنشئ ومحدد موقع مُخيط في عنصر واجهة المستخدم ، دون القدرة على تغييره. عادة ما يكون جدول المحتويات واحدًا على الصفحة ، ويمكن ترك طريقة البحث الخاصة به على الصفحة مع الإجراءات "الداخلية" مع جدول المحتويات ، ولا معنى لإخراج محدد الموقع بشكل منفصل ، حيث يمكن أن يتلف محدد الموقع عرضيًا من الخارج ، ولكن لا فائدة من تخزينه المنفصل. في المقابل ، يعد حقل النص شيئًا عالميًا ، على العكس من ذلك ، تحتاج إلى العمل معه فقط من خلال محدد المواقع من المنشئ ، لأنه يمكن أن يكون هناك العديد من حقول الإدخال في وقت واحد. إذا ظهرت طريقة واحدة على الأقل مخصصة فقط لحقل إدخال خاص واحد ، على سبيل المثال ، بنقرة إضافية على تلميح القائمة المنسدلة ، فهذا لم يعد مجرد حقل إدخال ، فقد حان الوقت لإنشاء عنصر واجهة المستخدم الخاص بك له.
لتقليل الفوضى العامة ، يتم دمج عناصر واجهة المستخدم ، مثل عناصر الصفحة ، في نفس الصفحات ، والتي يبدو أن اسم عنصر الصفحة يتكون منها.
public class UsersPage {
public Table usersTable = new Table();
public InputLine email = new InputLine(By.id("dataEmail"));
public InputLine password = new InputLine(By.id("dataPassword"));
public InputLine name = new InputLine(By.id("dataName"));
public DropdownList gender = new DropdownList(By.id("dataGender"));
public Checkbox var11 = new Checkbox(By.id("dataSelect11"));
public Checkbox var12 = new Checkbox(By.id("dataSelect12"));
public Checkbox var21 = new Checkbox(By.id("dataSelect21"));
public Checkbox var22 = new Checkbox(By.id("dataSelect22"));
public Checkbox var23 = new Checkbox(By.id("dataSelect23"));
public Button save = new Button(By.id("dataSend"));
public ErrorPopup errorPopup = new ErrorPopup();
public ModalPopup savePopup = new ModalPopup();
}
لاستخدام كل ما سبق تم إنشاؤه في الاختبارات ، تحتاج إلى الرجوع بالتسلسل إلى الصفحة ، والأداة ، والإجراء ، وبالتالي نحصل على البناء التالي:
@Test
public void authAsAdmin() {
baseRouter
.authPage().email.fill("test@protei.ru")
.authPage().password.fill("test")
.authPage().enter.click()
.mainPage().logoutButton.shouldExist();
}
يمكنك إضافة طبقة خطوة كلاسيكية إذا كانت هناك حاجة لذلك في إطار العمل الخاص بك (يتطلب تنفيذ المكتبة البعيدة في Java لـ RobotFramework فئة خطوة كإدخال ، على سبيل المثال) ، أو إذا كنت تريد إضافة تعليقات توضيحية لتقارير جميلة. لقد جعلناه منشئًا قائمًا على التعليقات التوضيحية ، إذا كنت مهتمًا ، فاكتب التعليقات ، وسنخبرك بذلك.
مثال على فئة خطوة التفويض
public class AuthSteps{
private BaseRouter baseRouter = new BaseRouter();
@Step("Sigh in as {mail}")
public BaseSteps login(String mail, String password) {
baseRouter
.authPage().email.fill(mail)
.authPage().password.fill(password)
.authPage().enter.click()
.mainPage().logoutButton.shouldExist();
return this;
}
@Step("Fill E-mail")
public AuthSteps fillEmail(String email) {
baseRouter.authPage().email.fill(email);
return this;
}
@Step("Fill password")
public AuthSteps fillPassword(String password) {
baseRouter.authPage().password.fill(password);
return this;
}
@Step("Click enter")
public AuthSteps clickEnter() {
baseRouter.authPage().enter.click();
return this;
}
@Step("Enter should exist")
public AuthSteps shouldExistEnter() {
baseRouter.authPage().enter.shouldExist();
return this;
}
@Step("Logout")
public AuthSteps logout() {
baseRouter.mainPage().logoutButton.click()
.authPage().enter.shouldExist();
return this;
}
}
public class BaseRouter {
// , ,
public AuthPage authPage() {return page(AuthPage.class);}
public MainPage mainPage() {return page(MainPage.class);}
public UsersPage usersPage() {return page(UsersPage.class);}
public VariantsPage variantsPage() {return page(VariantsPage.class);}
}
هذه الخطوات مشابهة جدًا للخطوات الموجودة داخل الصفحات ، ولا تختلف عمليًا. لكن فصلهم إلى فئات منفصلة يفتح مجالًا لإنشاء الكود ، بينما لا يتم فقد الرابط الصلب بالصفحة المقابلة. في الوقت نفسه ، إذا لم تكتب خطوات في الصفحة ، فسيختفي معنى التغليف ، وإذا لم تقم بإضافة فئة من الخطوات إلى pageElement ، فسيظل التفاعل مع الصفحة منفصلاً عن منطق الأعمال.
, , . . , , , « , ». — , page object , !
سيكون من الخطأ التحدث عن بنية المشروع دون التطرق إلى طرق التشغيل المريح مع بيانات الاختبار.
أسهل طريقة هي تمرير البيانات مباشرة في الاختبار "كما هي" أو في المتغيرات. هذا جيد للهندسة المعمارية الورقية ، لكن المشاريع الكبيرة تصبح فوضوية.
هناك طريقة أخرى وهي تخزين البيانات ككائنات ، وقد تبين أنها الأفضل بالنسبة لنا ، لأنها تجمع كل البيانات المتعلقة بكيان واحد في مكان واحد ، وتزيل الرغبة في خلط كل شيء واستخدام شيء ما في المكان الخطأ. بالإضافة إلى ذلك ، تحتوي هذه الطريقة على العديد من التحسينات الإضافية التي يمكن أن تكون مفيدة في المشاريع الفردية.
لكل كيان ، يتم إنشاء نموذج يصفه ، والذي يحتوي في أبسط الحالات على أسماء وأنواع الحقول ، على سبيل المثال ، هنا نموذج المستخدم:
public class User {
private Integer id;
private String mail;
private String name;
private String password;
private Gender gender;
private boolean check11;
private boolean check12;
private boolean check21;
private boolean check22;
private boolean check23;
public enum Gender {
MALE,
FEMALE;
public String getVisibleText() {
switch (this) {
case MALE:
return "";
case FEMALE:
return "";
}
return "";
}
}
}
اختراق الحياة رقم 1: إذا كان لديك بنية تشبه الراحة للتفاعل بين العميل والخادم (تنتقل كائنات json أو xml بين العميل والخادم ، وليس أجزاء من التعليمات البرمجية غير القابلة للقراءة) ، فيمكنك حينئذٍ google json إلى كائن <your language> ، فربما يكون لديك بالفعل المولد المطلوب ...
اختراق الحياة # 2: إذا كتب مطورو الخادم الخاص بك بنفس لغة البرمجة الموجهة للكائنات ، فيمكنك استخدام نماذجهم.
اختراق الحياة رقم 3: إذا كنت من هواة جافا وتسمح لك الشركة باستخدام مكتبات الطرف الثالث ، ولا يوجد زملاء متوترون حولك ، ويتوقعون الكثير من الألم للزنادقة الذين يستخدمون مكتبات إضافية بدلاً من جافا النقية والجميلة ، خذ لومبوك ! نعم ، عادة IDEيمكن أن تولد حاصل ، محدد ، toString وبناة. ولكن عند مقارنة نماذج Lombok الخاصة بنا ونماذج التطوير بدون Lombok ، يمكن رؤية ربح مئات الأسطر من الكود "الفارغ" الذي لا يحمل منطق الأعمال لكل فئة. عند استخدام Lombok ، لن تضطر إلى التغلب على أيدي أولئك الذين يخلطون بين الحقول والأدوات ، فالصف أسهل في القراءة ، ويمكنك الحصول على فكرة عن الكائن مرة واحدة ، دون التمرير عبر ثلاث شاشات.
وبالتالي ، لدينا إطارات سلكية للكائنات التي نحتاج إلى تمديد بيانات الاختبار عليها. يمكن تخزين البيانات كمتغيرات ثابتة نهائية ، على سبيل المثال ، يمكن أن يكون هذا مفيدًا لمسؤول النظام الرئيسي ، والذي يتم من خلاله إنشاء مستخدمين آخرين. من الأفضل استخدام النهائي ، حتى لا يكون هناك إغراء لتغيير البيانات في الاختبارات ، لأن الاختبار التالي ، بدلاً من المسؤول ، يمكن أن يحصل على مستخدم "ضعيف" ، ناهيك عن الإطلاق الموازي للاختبارات.
public class Users {
public static final User admin = User.builder().mail("test@protei.ru").password("test").build();
}
للحصول على البيانات التي لا تؤثر على الاختبارات الأخرى ، يمكنك استخدام نمط "النموذج الأولي" واستنساخ المثيل الخاص بك في كل اختبار. قررنا أن نجعل الأمر أسهل: أن تكتب عملية تقوم بترتيب حقول الفصل بشكل عشوائي ، شيء من هذا القبيل:
public static User getUserRandomData() {
User user = User.builder()
.mail(getRandomEmail())
.password(getShortLatinStr())
.name(getShortLatinStr())
.gender(getRandomFromEnum(User.Gender.class))
.check11(getRandomBool())
.check21(getRandomBool())
.check22(getRandomBool())
.check23(getRandomBool())
.build();
//business-logic: 11 xor 12 must be selected
if (!user.isCheck11()) user.setCheck12(true);
if (user.isCheck11()) user.setCheck12(false);
return user;
}
في الوقت نفسه ، يجب وضع الطرق التي تنشئ عشوائية مباشرة في فئة منفصلة ، حيث سيتم استخدامها في نماذج أخرى أيضًا:
في طريقة الحصول على مستخدم عشوائي ، تم استخدام نمط "builder" ، وهو أمر ضروري حتى لا يتم إنشاء نوع جديد من المُنشئ لكل مجموعة مطلوبة مجالات. بدلاً من ذلك ، بالطبع ، يمكنك ببساطة استدعاء المُنشئ المطلوب.
تستخدم طريقة تخزين البيانات هذه نمط كائن القيمة ، والذي يمكنك بناءً عليه إضافة أي من رغباتك ، اعتمادًا على احتياجات المشروع. يمكنك إضافة تخزين العناصر إلى قاعدة البيانات ، وبالتالي تحضير النظام قبل الاختبار. لا يمكنك اختيار المستخدمين بشكل عشوائي ، ولكن تحميلهم من ملفات الخصائص ( ومكتبة أخرى رائعة). يمكنك استخدام نفس المستخدم في كل مكان ، ولكن قم بإنشاء ما يسمى بتسجيل البيانات لكل نوع من الكائنات ، حيث ستتم إضافة قيمة العداد من طرف إلى طرف إلى الاسم أو أي حقل فريد آخر للكائن ، وسيكون للاختبار دائمًا testUser_135 الخاص به.
يمكنك كتابة تخزين الكائنات الخاص بك (مجموعة كائنات google و flyweight) ، والتي يمكنك من خلالها طلب الكيانات اللازمة في بداية الاختبار. يعطي المستودع أحد عناصره الجاهزة للاستخدام ويضع علامة على أنه مشغول. في نهاية الاختبار ، يتم إرجاع الكائن إلى التخزين ، حيث يتم تنظيفه إذا لزم الأمر ، ووضع علامة "مجاني" عليه ، وإعطائه للاختبار التالي. يتم ذلك إذا كانت عمليات إنشاء الكائنات كثيفة الاستخدام للموارد ، وبهذا النهج ، يعمل التخزين بشكل مستقل عن الاختبارات ويمكنه إعداد البيانات للحالات التالية.
إنشاء البيانات
بالنسبة لحالات تحرير المستخدم ، ستحتاج بالتأكيد إلى مستخدم تم إنشاؤه ستقوم بتحريره ، بينما ، بشكل عام ، لا يهتم اختبار التحرير من أين جاء هذا المستخدم. هناك عدة طرق لإنشائه:
- اضغط على الأزرار بيديك قبل الاختبار ،
- ترك البيانات من الاختبار السابق ،
- نشر قبل الاختبار من النسخ الاحتياطي ،
- إنشاء من خلال النقر على الأزرار مباشرة في الاختبار ،
- استخدام API.
كل هذه الطرق لها عيوب: إذا كنت بحاجة إلى إدخال شيء ما في النظام يدويًا قبل الاختبار ، فهذا اختبار سيء ، وبالتالي يطلق عليها الاختبارات التلقائية ، لأنها يجب أن تعمل بشكل مستقل عن الأيدي البشرية قدر الإمكان.
استخدام نتائج الاختبار السابق ينتهك مبدأ الذرية ولا يسمح لك بإجراء الاختبار بشكل منفصل ، فسيتعين عليك تشغيل الدفعة بأكملها ، ولا تكون اختبارات واجهة المستخدم بهذه السرعة. يعتبر كتابة الاختبارات طريقة جيدة بحيث يمكن تشغيل كل منها في عزلة رائعة وبدون رقصات إضافية. بالإضافة إلى ذلك ، لا يضمن وجود خطأ في إنشاء كائن أسقط الاختبار السابق على الإطلاق وجود خطأ في التحرير ، وفي مثل هذا البناء ، سيأتي اختبار التحرير بعد ذلك ، ومن المستحيل معرفة ما إذا كان التحرير يعمل أم لا.
يعد استخدام النسخ الاحتياطي (صورة محفوظة لقاعدة البيانات) مع البيانات اللازمة للاختبار طريقة جيدة إلى حد ما أو أقل ، خاصةً إذا تم نشر النسخة الاحتياطية تلقائيًا أو إذا كانت الاختبارات نفسها تضع البيانات في قاعدة البيانات. ومع ذلك ، فإن سبب استخدام هذا الكائن المعين في الاختبار ليس واضحًا ، ويمكن أن تبدأ مشاكل تقاطع البيانات أيضًا بعدد كبير من الاختبارات. أحيانًا يتوقف النسخ الاحتياطي عن العمل بشكل صحيح بسبب تحديث بنية قاعدة البيانات ، على سبيل المثال ، إذا كنت بحاجة إلى إجراء اختبارات على إصدار قديم ، وتحتوي النسخة الاحتياطية بالفعل على حقول جديدة. يمكنك محاربة هذا من خلال تنظيم تخزين احتياطي لكل إصدار من التطبيق. أحيانًا يتوقف النسخ الاحتياطي عن كونه صالحًا مرة أخرى بسبب تحديث بنية قاعدة البيانات - تظهر الحقول الجديدة بانتظام ، لذلك يجب تحديث النسخة الاحتياطية بانتظام. وفجأة قد يكونأن مثل هذا المستخدم الفردي من النسخ الاحتياطي لا يتعطل أبدًا ، وإذا كان المستخدم قد تم إنشاؤه للتو أو تم إعطاء الاسم له بشكل عشوائي قليلاً ، فستجد خطأ. وهذا ما يسمى "تأثير المبيدات" ، حيث يتوقف الاختبار عن اصطياد البق ، لأن التطبيق "مستخدم" لنفس البيانات ولا يسقط ، ولا توجد انحرافات في الجانب.
إذا تم إنشاء المستخدم في الاختبار من خلال النقرات على نفس الواجهة ، فإن المبيد ينخفض ويختفي عدم وضوح مظهر المستخدم. تتشابه العيوب مع استخدام نتائج الاختبار السابق: السرعة جيدة ، وحتى إذا كان هناك خطأ في الإنشاء ، حتى أصغرها (خاصة خطأ الاختبار ، على سبيل المثال ، سيتغير محدد موقع زر الحفظ) ، فلن نعرف ما إذا كان التحرير يعمل.
أخيرًا ، هناك طريقة أخرى لإنشاء مستخدم وهي من خلال http-API من الاختبار ، أي بدلاً من النقر على الأزرار ، أرسل طلبًا على الفور لإنشاء المستخدم المطلوب. وبالتالي ، يتم تقليل المبيدات قدر الإمكان ، ومن الواضح من أين جاء المستخدم ، وتكون سرعة الإنشاء أعلى بكثير مما كانت عليه عند النقر على الأزرار. عيوب هذه الطريقة هي أنها غير مناسبة للمشاريع التي لا تحتوي على json أو xml في بروتوكول الاتصال بين العميل والخادم (على سبيل المثال ، إذا كتب المطورون باستخدام gwt ولا يريدون كتابة واجهة برمجة تطبيقات إضافية للمختبرين). من الممكن ، عند استخدام واجهة برمجة التطبيقات ، فقدان جزء من المنطق الذي تم تنفيذه بواسطة لوحة الإدارة ، وإنشاء كيان غير صالح. يمكن أن تتغير واجهة برمجة التطبيقات ، مما يتسبب في فشل الاختبارات ، ولكن عادةً ما يكون هذا معروفًا ، ولا يحتاج أحد إلى تغييرات من أجل التغييرات ، وعلى الأرجح هذا منطق جديد لا يزال يتعين التحقق منه.من الممكن أيضًا أن يكون هناك خطأ على مستوى واجهة برمجة التطبيقات ، ولكن لا توجد طريقة واحدة بخلاف النسخ الاحتياطية الجاهزة آمنة من هذا ، لذلك من الأفضل الجمع بين الأساليب لإنشاء البيانات.
إضافة تطبيق droplet API
من بين طرق إعداد البيانات ، يعد http-API للاحتياجات الحالية لاختبار منفصل ونشر نسخة احتياطية لبيانات الاختبار الإضافية التي لا تتغير في الاختبارات ، على سبيل المثال ، رموز الكائنات ، بحيث لا تتعطل اختبارات هذه الكائنات عند تحميل الرموز ، هي الأنسب لنا.
لإنشاء كائنات من خلال API في Java ، اتضح أن استخدام مكتبة restAssured الأكثر ملاءمة ، على الرغم من أنها ليست مخصصة لهذا الغرض. أريد أن أشارك اثنين من الرقائق التي تم العثور عليها ، أنت تعرف المزيد - اكتب!
الألم الأول هو الإذن في النظام. يجب تحديد طريقته بشكل منفصل لكل مشروع ، ولكن هناك شيء واحد مشترك - يجب وضع التفويض في مواصفات الطلب ، على سبيل المثال:
public class ApiSettings {
private static String loginEndpoint="/login";
public static RequestSpecification testApi() {
RequestSpecBuilder tmp = new RequestSpecBuilder()
.setBaseUri(testConfig.getSiteUrl())
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.addFilter(new BeautifulRest())
.log(LogDetail.ALL);
Map<String, String> cookies = RestAssured.given().spec(tmp.build())
.body(admin)
.post(loginEndpoint).then().statusCode(200).extract().cookies();
return tmp.addCookies(cookies).build();
}
}
يمكنك إضافة القدرة على حفظ ملفات تعريف الارتباط لمستخدم معين ، ثم سينخفض عدد الطلبات إلى الخادم. الامتداد الثاني المحتمل لهذه الطريقة هو حفظ ملفات تعريف الارتباط المستلمة للاختبار الحالي ، وإلقاءها في برنامج تشغيل المتصفح ، وتخطي خطوة التفويض. المكسب هو الثواني ، ولكن إذا قمت بضربها في عدد الاختبارات ، يمكنك الإسراع بشكل جيد!
هناك كعكة للمشي وتقارير جميلة ، انتبه إلى الخط
.addFilter(new BeautifulRest()):
فئة الراحة الجميلة
public class BeautifulRest extends AllureRestAssured {
public BeautifulRest() {}
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) {
AllureLifecycle lifecycle = Allure.getLifecycle();
lifecycle.startStep(UUID.randomUUID().toString(), (new StepResult()).setStatus(Status.PASSED).setName(String.format("%s: %s", requestSpec.getMethod(), requestSpec.getURI())));
Response response;
try {
response = super.filter(requestSpec, responseSpec, filterContext);
} finally {
lifecycle.stopStep();
}
return response;
}
}
تتلاءم نماذج الكائنات تمامًا مع restAssured ، نظرًا لأن المكتبة نفسها تتعامل مع التسلسل وإلغاء تسلسل النماذج في json / xml (التحويل من تنسيقات json / xml إلى كائن من فئة معينة).
@Step("create user")
public static User createUser(User user) {
String usersEndpoint = "/user";
return RestAssured.given().spec(ApiSettings.testApi())
.when()
.body(user)
.post(usersEndpoint)
.then().log().all()
.statusCode(200)
.body("state",containsString("OK"))
.extract().as(User.class);
}
إذا كنت تفكر في عدة خطوات متتالية لإنشاء كائنات ، يمكنك ملاحظة هوية الرمز. لتقليل نفس الرمز ، يمكنك كتابة طريقة عامة لإنشاء الكائنات.
public static Object create(String endpoint, Object model) {
return RestAssured.given().spec(ApiSettings.testApi())
.when()
.body(model)
.post(endpoint)
.then().log().all()
.statusCode(200)
.body("state",containsString("OK"))
.extract().as(model.getClass());
}
@Step("create user")
public static User createUser(User user) {
create(User.endpoint, user);
}
مرة أخرى حول العمليات الروتينية
كجزء من التحقق من تحرير كائن ، لا نهتم بشكل عام بكيفية ظهور الكائن في النظام - عبر واجهة برمجة التطبيقات أو من النسخ الاحتياطي ، أو تم إنشاؤه بواسطة اختبار واجهة المستخدم. تتمثل الإجراءات المهمة في العثور على كائن ، والنقر فوق رمز "تحرير" عليه ، ومسح الحقول واملأها بقيم جديدة ، ثم انقر فوق "حفظ" وتحقق مما إذا تم حفظ جميع القيم الجديدة بشكل صحيح. يجب إزالة جميع المعلومات غير الضرورية التي لا ترتبط ارتباطًا مباشرًا بالاختبار بطرق منفصلة ، على سبيل المثال ، في فئة الخطوة.
@Test
void checkUserVars() {
//Arrange
User userForTest = getUserRandomData();
// ,
// - ,
// ,
usersSteps.createUser(userForTest);
authSteps.login(userForTest);
//Act
mainMenuSteps
.clickVariantsMenu();
//Assert
variantsSteps
.checkAllVariantsArePresent(userForTest.getVars())
.checkVariantsCount(userForTest.getVarsCount());
//Cleanup
usersSteps.deleteUser(userForTest);
}
من المهم عدم الانجراف ، لأن الاختبار الذي يتكون فقط من إجراءات "معقدة" يصبح أقل قابلية للقراءة ويصعب إعادة إنتاجه دون البحث في الشفرة.
@Test
void authAsAdmin() {
authSteps.login(Users.admin);
// , . .
// , ?
إذا ظهرت نفس الاختبارات عمليًا في المجموعة ، والتي تختلف فقط في إعداد البيانات (على سبيل المثال ، تحتاج إلى التحقق من أن جميع الأنواع الثلاثة من المستخدمين "المختلفين" يمكنهم تنفيذ نفس الإجراءات ، أو أن هناك أنواعًا مختلفة من كائنات التحكم ، والتي تحتاج إلى التحقق من كل منها إنشاء كائنات تابعة متطابقة ، أو تحتاج إلى التحقق من التصفية بواسطة عشرة أنواع من حالات الكائنات) ، لا يزال يتعذر عليك نقل الأجزاء المكررة إلى طريقة منفصلة. لا على الإطلاق إذا كانت سهولة القراءة مهمة بالنسبة لك!
بدلاً من ذلك ، عليك أن تقرأ عن الاختبارات التي تعتمد على البيانات ، بالنسبة لـ Java + TestNG سيكون شيئًا من هذا القبيل:
@Test(dataProvider = "usersWithDifferentVars")
void checkUserDifferentVars(User userForTest) {
//Arrange
usersSteps.createUser(userForTest);
authSteps.login(userForTest);
//Act
mainMenuSteps
.clickVariantsMenu();
//Assert
variantsSteps
.checkAllVariantsArePresent(userForTest.getVars())
.checkVariantsCount(userForTest.getVarsCount());
}
// .
// , -.
@DataSupplier(name = "usersWithDifferentVars")
public Stream<User> usersWithDifferentVars(){
return Stream.of(
getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(false),
getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(false),
getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(false),
getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(true),
getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(false),
getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(true),
getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(true),
getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(true)
);
}
يستخدم مكتبة مزود البيانات ، وهي وظيفة إضافية عبر موفر بيانات TestNG والتي تتيح لك استخدام المجموعات المكتوبة بدلاً من الكائن [] [] ، ولكن الجوهر هو نفسه. وبالتالي ، نحصل على اختبار واحد ، يتم تنفيذه عدة مرات حيث يتلقى بيانات الإدخال.
الاستنتاجات
لذلك ، لإنشاء مشروع كبير ولكنه مناسب للاختبارات التلقائية لواجهة المستخدم ، فإنك تحتاج إلى:
- وصف جميع الحاجيات الصغيرة التي تمت مواجهتها في التطبيق ،
- جمع الأدوات في صفحات ،
- إنشاء نماذج لجميع أنواع الكيانات ،
- إضافة طرق لإنشاء جميع أنواع الكيانات بناءً على النماذج ،
- فكر في طريقة مناسبة لإنشاء كيانات إضافية
- اختياري: إنشاء ملفات الخطوات أو تجميعها يدويًا ،
- اكتب الاختبارات بحيث لا توجد إجراءات معقدة في قسم الإجراءات الرئيسية لاختبار معين ، فقط عمليات واضحة باستخدام عناصر واجهة المستخدم.
تم الانتهاء من إنشاء مشروع يستند إلى PageElement بأساليب بسيطة لتخزين البيانات وتوليدها وإعدادها. لديك الآن بنية يسهل صيانتها وإدارتها ومرنة بدرجة كافية. يمكن لكل من المختبرين ذوي الخبرة والمبتدئين في يونيو التنقل بسهولة في المشروع ، نظرًا لأن الاختبارات التلقائية في تنسيق إجراءات المستخدم هي الأكثر ملاءمة للقراءة والفهم.
تمت إضافة أمثلة التعليمات البرمجية من المقالة في شكل مشروع منتهي إلى البوابة .