تست مرحله به مرحله Angular

تست مرحله به مرحله شیوه‌ای برای تست برنامه از دیدگاه کاربر است. تست‌ها اجرای برنامه از ابتدا تا انتها همان‌گونه که انتظار می‌رود را تضمین می‌کنند. همان‌طور که تست‌ها اجرا می‌شوند، تعامل مرورگر را فقط به عنوان کاربری که از برنامه استفاده می‌کند، مشاهده می‌کنید. تست مرحله‌ای انگولار توسط فریم‌ورکی به نام Protractor به صورت قدرتمند انجام می‌شود.

تست مرحله به مرحله Angular

Protractor یک فریم‌ورک تست مرحله‌ای برای برنامه‌های انگولار است. Protractor تست‌ها را در قبال اجرای برنامه شما در مرورگر واقعی اجرا می‌کند و با آن همانند یک کاربر در تعامل است.

Protractor بر روی بهترین Selenium Webdriver اجرا می شود که APIای برای اتوماسیون و تست مرورگر است.

در این مقاله موارد زیر را خواهیم آموخت:

بررسی برنامه ‌یمان

پیکربندی و اجرای تست مرحله‌ای

Jasmine

Protractor API

Page Object

نتیجه‌گیری

بررسی برنامه‌یمان

ما از برنامه‌ای استفاده می‌کنیم که قبلا آن را ساخته‌ایم. می‌توانید به گیت هاب مراجعه کرده و آن را از اینجا بگیرید. در بخش Page Object رفرنس‌هایی را برای آن ایجاد خواهیم کرد.

برنامه یک صفحه اصلی و یک صفحه آلبوم دارد. ما عناصر و توابع هر دو صفحه را تست خواهیم کرد.

پیکربندی و اجرای مرحله‌ای تست‌ها

هنگام استفاده از Angular CLI، ساختار دایرکتوری مانند زیر است:

اجرای تست protractor بستگی به دو فایل دارد. فایل‌های spec درون فولدر e2e و فایل protractor.conf.json. بیایید ادامه دهیم و به فایل protractor.conf.json نگاهی بیندازیم.

// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

خط directConnect: true به Protractor اجازه می دهد تا به درایورهای مرورگر متصل شود. مرورگرهای پشتیبانی‌شده در حال حاضر کروم، فایرفاکس، سافاری و اینترنت اکسپلورر هستند. اگر اجرای تست‌ها را در مرورگر نام برده شده انجام می دهید، می‌توانید از این فایل‌ها به همان صورت که هست بگذرید. اگر از مرورگری متفاوت از آن‌هایی که در بالا ذکر شد استفاده می‌کنید، باید سرور Selenium را اجرا کنید. مراحل همانند زیر است:

1. نصب سراسری Protractor

npm install -g protractor

2. به‌روزرسانی webdriver-manager برای استفاده از باینری‌هایی که به روز هستند

webdriver-manager update

3. راه‌اندازی سرور Selenium

webdriver-manager start

در protractor.conf.js خود directConnect: false را تغییر دهید و seleniumAddress: 'http://localhost:4444/wd/hub' را اضافه کنید. فایل شما باید همانند دستور زیر شود:

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: false,
  baseUrl: 'http://localhost:4200/',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

در این مثال ما از درایورهای مرورگر استفاده خواهیم کرد. بنابراین هیچ تغییری در فایل protractor.conf.js ایجاد نخواهیم کرد.

برای اجرای تست دستور ng e2e را وارد کنید.

Jasmine

Jasmine یک فریم‌ورک تست خودکار برای جاوااسکریپت است. در فایل‌های spec تست‌ها را با استفاده از سینتکس Jasmine نوشتیم. در اینجا نمونه‌ای از سینتکس jasmine را مشاهده می‌کنید.

describe('Exciting App', () => {

  beforeEach(() => { 
  });

  it('should display welcome message', () => {
    browser.get('/home');
    expect(element(by.id('page-title').getText()).toEqual('Welcome to app!!');
  });
});

بلوک describe یک دنباله نامیده می شود که اساسا جزئی از برنامه است. بلوک های describe تست‌ها را درون دنباله‌ای از تست‌های منطقی تقسیم می کنند. یک بلوک describe می‌تواند متد beforeEach() که حاوی کدی است که قبل از هر تست اجرا می‌شود را داشته باشد. درون یک بلوک describe می‌تواند چندین بلوک وجود داشته باشد. این‌ها مشخصات مختلفی هستند که باید تست شوند. در مثال فوق تست spec پیام خوش‌آمدگویی را نمایش می‌دهد. انتظارات با توابع expect ساخته می‌شوند که باید یک مقدار (واقعی) را بگیرند. با یک تابع Matcher ایجاد می شود که مقادیر مورد انتظار را می‌گیرد. اگر مقادیر واقعی و مورد انتظار با تست مطابقت داشته باشند تست با موفقیت انجام می شود درغیر این صورت شکست می‌خورد. درون تابع expect، )()element(by.id('page-title').getText را داریم. به زودی از این سینتکس استفاده خواهیم کرد.

Protractor API

در مثال بالا، همان‌طور که ملاحظه می کنید، browser.get('/home’); و element(by.id('page-title').getText() را داریم. این‌ها بخشی از protractor API هستند.

بیایید به برخی از protractor API سراسری نگاهی بیندازیم.

browser برای دستورات سطح مرورگر استفاده می شود مثل هدایت با تابع browser.get element که برای عناصر HTML صفحه وب استفاده می‌شود. این یک شیء ElementFinder را برمی‌گرداند که می‌تواند برای تعامل با اطلاعات یافته ‌شده در مورد عنصر استفاده شود. element یک آرگومان، یک جایگزین‌شونده (Locator) می‌گیرد که نحوه یافتن عنصر را توصیف می‌کند.

در اینجا برخی از Locatorهایی که در این آموزش استفاده می‌شود را می‌بینید:

('')by.css: برای پیدا کردن یک عنصر بر اساس انتخابگر css استفاده می شود.

('')by.tagName: برای پیدا کردن یک عنصر بر اساس نام برچسب (tag) استفاده می‌شود.

Page Object

اگر پروژه‌ای را با استفاده از angular cli ایجاد کرده‌اید و به فولدر e2e  نگاه کنید متوجه خواهید شد دو فایل وجود دارد.

ما فایل spec را داریم که protractor برای اجرای تست استفاده می‌کند. سپس فایل po.ts. را داریم. این فایل Page Object ما است.

در پروژه ما ساختار فولدر e2e کمی متفاوت است. ما آن‌ها را با مطابقت با فولدر app خود که در واقع عملکرد خوبی دارد ایجاد کرده‌ایم. این تست‌ها به صورت منظم می‌باشند. ساختار فولدر ما اینگونه است:

Page Object یک الگوی طراحی است که در اتوماسیون تست برای بالا بردن قابلیت نگهداری تست و کاهش کد تکراری محبوب شده است. page object یک کلاس شی‌ءگرا است که به عنوان رابط صفحه AUT شما عمل می‌کند. سپس تست‌ها هر زمان که نیاز به تعامل با UI صفحه داشته باشند، از متدهای این کلاس page object استفاده می‌کنند. مزیت آن این است که اگر UI صفحه تغییر کند، تست‌ها خودشان نیازی به تغییر ندارند، فقط کد درون page object نیاز به تغییر دارد. پس از آن همه تغییرات برای پشتیبانی از UI جدید در یک مکان قرار می‌گیرند.

بیایید به فایل home-page.po.ts نگاهی بیندازیم.

// e2e/pages/home-page/home-page.po.ts

import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';

export class HomePage {

    navigateToHome(): promise.Promise<any> {
        return browser.get('/home');
    }

    getPageBrandName(): promise.Promise<string> {
        return element(by.css('.masthead-brand')).getText();
    }

    getNavBar(): ElementFinder {
        return element(by.tagName('nav'));
    }

    getAlbumButton(): ElementFinder {
        return this.getNavBar().all(by.css('a')).get(1);
    }

    getLearnMoreButton(): ElementFinder {
        return element(by.css('.lead a'));
    }
}

با توجه به فایل‌مان می‌توانیم ببینیم که در کلاس HomePage، متدهای مختلفی را برای یافتن عناصر موجود در صفحه داریم. با توجه به فایل spec هم می‌توانیم سپس این متدها را برای تست عناصر مختلف فراخوانی کنیم. بیایید به فایل home-page.e2e-spec.ts نگاهی بیندازیم.

// e2e/pages/home-page/home-page.e2e-spec.ts

import { browser } from 'protractor';
import { HomePage } from './home-page.po';

describe(' Home Page', () => {
    const homePage = new HomePage();

    beforeEach(() => {
        homePage.navigateToHome();
    });

    it('Should have the page brand name', () => {
        expect(homePage.getPageBrandName()).toEqual('Plane Spotters');
    });

    it('Should locate the nav bar', () => {
        expect(homePage.getNavBar()).toBeDefined();
    });

    it('Should get the album button on the nav bar', () => {
        expect(homePage.getAlbumButton().getText()).toEqual('Album');
    });

    it('Should redirect to the album page when album is clicked', () => {
        const album = homePage.getAlbumButton();
        album.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });

    it('Should find the learn more button', () => {
        expect(homePage.getLearnMoreButton().getText()).toEqual('Learn more');
    });

    it('Should redirect to the album page when learn more is clicked', () => {
        const learnMore = homePage.getLearnMoreButton();
        learnMore.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });
});

تست‌ها در فایل spec با استفاده از سینتکس jasmine که در مورد آن صحبت کردیم، نوشته می‌شوند. در بلوک describe ابتدا یک نمونه از کلاس HomePage که قبلا در فایل homepage.po.ts ایجاد کرده بودیم، ساختیم. سپس با متد beforeEach() هدایت را به صفحه اصلی قرار دادیم. این کار فقط باعث می‌شود که مطمئن شویم که قبل از اجرای هر تست در صفحه اصلی هستیم. توجه داشته باشید که از متد browser.get() استفاده نکردیم، قبلا آن را در فایل page object انجام دادیم. همه ما باید حالا فراخوانی این متد را مانند homePage.navigateToHome(); انجام دهیم.

از فایل page objects، می توانیم عناصر را در صفحه قرار دهیم. هنگام اجرای تست‌ها می‌توانیم اقدامات مختلفی، مثل click()،getText() و sendKeys()را روی این عناصر انجام دهیم. یک نمونه خوب اینگونه است:

    it('Should redirect to the album page when album is clicked', () => {
        const album = homePage.getAlbumButton();
        album.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });

ما دکمه album را می‌زنیم سپس اکشن click را روی آن اجرا می‌کنیم. در مرورگر هنگام کلیک روی album باید به صفحه آلبوم هدایت شویم. بنابراین در expect، بررسی می‌کنیم که آیا Url به صفحه آلبوم تغییر کرده است یا خیر. انتظارات زیادی وجود دارد که می‌توانید آن‌ها را تست کنید، مثل toBeDefined()،toEqual() و toContain().

در فولدر album-page ما تست‌های مشابه‌ای را نوشته‌ایم. در اینجا فایل album-page.po.ts وجود دارد:

// e2e/pages/album-page/album-page.po.ts

import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
import { Element } from '@angular/compiler';

export class AlbumPage {

    navigateToAlbumPage(): promise.Promise<any> {
        return browser.get('/album');
    }

    getHomeNavigationButton(): ElementFinder {
        return element(by.css('.navbar-brand'));
    }

    getImages(): ElementArrayFinder {
        return element.all(by.css('.row div'));
    }

    getImagePopUpModal(): ElementFinder {
        return element(by.tagName('app-modal'));
    }
}

و دستورات زیر فایل album-page.e2e-spec.ts ما است:

// e2e/pages/album-page/album-page.e2e-spec.ts

import { browser } from 'protractor';
import { AlbumPage } from './album-page.po';

describe('Album Page', () => {

    const albumPage = new AlbumPage();

    beforeEach(() => {
        albumPage.navigateToAlbumPage();
    });

    it('Should find the home navigation button', () => {
        expect(albumPage.getHomeNavigationButton().getText()).toContain('Home');
    });

    it('Should redirect to the home page when \'Home\' is clicked', () => {
        const homeButton = albumPage.getHomeNavigationButton();
        homeButton.click();
        expect(browser.driver.getCurrentUrl()).toContain('/home');
    });

    it('Should have 6 images in the album page', () => {
        expect(albumPage.getImages().count()).toEqual(6);
    });

    it('Should open up a modal when an image is clicked', () => {
        const firstImage = albumPage.getImages().get(0);
        firstImage.click();
        expect(albumPage.getImagePopUpModal().isDisplayed()).toBeTruthy();
    })
});

نتیجه‌گیری

پس از انجام تست‌ها، وقتی دستور ng e2e را اجرا می‌کنیم متوجه می‌شویم که برنامه ما در مرورگر اجرا می‌شود و در ترمینال تمام تست‌ها را مشاهده می‌کنیم.

در این مقاله ما مباحث زیادی را پوشش دادیم. ما با تست‌های‌ مرحله‌ به مرحله شروع کرده، سپس در مورد protractor و jasmine صحبت کردیم. همچنین page objectها را پوشش داده و در مورد اهمیت آن‌ها وقتی تست‌های مرحله‌ای را می‌نویسیم صحبت کردیم.

امیدواریم این مباحث را به خوبی یاد گرفته باشید و بتوانید تست‌های خود را بنویسید.