تست رگرسیون بصری با سرو خالص


هر بار که نسخه جدیدی از کتابخانه مؤلفه ما، پیکاسو، منتشر می‌شود، ما همه برنامه‌های کاربردی جلویی خود را به‌روزرسانی می‌کنیم تا از ویژگی‌های جدید حداکثر بهره را ببریم و طرح‌های خود را در تمام قسمت‌های سایتمان تراز کنیم.

ماه گذشته، به‌روزرسانی پیکاسو را در پورتال استعدادهای برتر Toptal ارائه کردیم، پلتفرمی که استعدادهای ما برای یافتن شغل و تعامل با مشتریان از آن استفاده می‌کنند. دانستن اینکه انتشار با تغییرات عمده در طراحی همراه خواهد بود، و در تلاش برای به حداقل رساندن مشکلات غیرمنتظره، استفاده از تکنیک‌های تست رگرسیون بصری برای کمک به یافتن مشکلات قبل از انتشار منطقی بود.

تست رگرسیون بصری مفهوم جدیدی نیست. بسیاری از پروژه های دیگر در Toptal از آن استفاده می کنند، از جمله خود پیکاسو.

ابزارهایی مانند Percy، Happo و Chromatic را می‌توان برای کمک به تیم‌ها در ایجاد خط لوله رگرسیون بصری سالم استفاده کرد، و ما در ابتدا اضافه کردن آنها را در نظر گرفتیم. ما در نهایت تصمیم گرفتیم که فرآیند راه اندازی بسیار زمان بر باشد و می تواند برنامه ما را از مسیر خارج کند. ما قبلاً تاریخ تعیین شده ای برای مسدود کردن کد برای شروع مهاجرت تعیین کرده بودیم و تنها چند روز تا پایان مهلت باقی مانده بود، چاره ای جز خلاقیت نداشتیم.

تست رگرسیون بصری از طریق تست UI

در حالی که ما تست های رگرسیون بصری در پروژه نداشتیم، پوشش خوبی از تست های یکپارچه سازی UI با استفاده از Cypress داشتیم. اگرچه این ابزار بیشتر برای آن استفاده نمی شود، Cypress یک صفحه در اسناد خود اختصاص داده است تست بصری و دیگری که همه موارد را فهرست می کند پلاگین های موجود برای کمک به پیکربندی Cypress برای آزمایش بصری.

از سرو تا اسکرین شات

پس از بررسی مستندات موجود، تصمیم گرفتیم که cypress-snapshot-plugin را امتحان کنیم. راه اندازی فقط چند دقیقه طول کشید، و زمانی که این کار را انجام دادیم، به سرعت متوجه شدیم که به دنبال خروجی رگرسیون بصری سنتی نیستیم.

اکثر ابزارهای رگرسیون بصری با مقایسه عکس‌های فوری و تشخیص تفاوت‌های پیکسلی بین خط پایه شناخته شده و پذیرفته‌شده و نسخه اصلاح‌شده یک صفحه یا یک مؤلفه، به شناسایی تغییرات ناخواسته کمک می‌کنند. اگر اختلاف پیکسل بیشتر از یک آستانه تحمل تنظیم شده باشد، صفحه یا مؤلفه پرچمدار می شود تا به صورت دستی بررسی شود. با این حال، در این نسخه، می‌دانستیم که چندین تغییر کوچک در اکثر اجزای رابط کاربری خود خواهیم داشت، بنابراین تعیین آستانه قابل اجرا نیست. حتی اگر یک مؤلفه 100٪ متفاوت باشد، ممکن است در متن نسخه جدید درست باشد. به طور مشابه، انحراف به کوچکی چند پیکسل می تواند به این معنی باشد که یک جزء در حال حاضر برای تولید مناسب نیست.

اسکرین شات که نتیجه مورد انتظار و نتیجه واقعی آزمایش را نشان می دهد.
شکل 1. مثالی از تفاوت های جزئی پیکسل که منجر به منفی کاذب می شود

در آن مرحله، دو چیز متضاد مشخص شد: توجه به تفاوت‌های پیکسلی به شناسایی مشکلات کمکی نمی‌کرد، و مقایسه جانبی اجزا دقیقاً همان چیزی بود که ما به آن نیاز داشتیم. افزونه snapshot را کنار گذاشتیم و شروع به ایجاد مجموعه ای از تصاویر با اجزای خود قبل و بعد از اعمال به روز رسانی پیکاسو کردیم. به این ترتیب، می‌توانیم به سرعت تمام تغییرات را اسکن کنیم تا مشخص کنیم آیا نسخه‌های جدید همچنان با نیازهای سایت و استانداردهای کتابخانه مطابقت دارند یا خیر.

طرح جدید این بود که یک اسکرین شات از یک جزء گرفته شود، آن را به صورت محلی ذخیره کند، یک اسکرین شات جدید از همان جزء در شعبه با نسخه به روز شده پیکاسو گرفته شود و سپس آنها را در یک تصویر واحد ادغام کند. در نهایت، این رویکرد جدید با آنچه که ما با آن شروع کردیم تفاوت چندانی نداشت، اما در مرحله اجرا به ما انعطاف‌پذیری بیشتری داد زیرا دیگر نیازی به وارد کردن افزونه و استفاده از دستورات جدید آن نداشتیم.

نموداری که یک جریان مقایسه بصری را نشان می دهد، چگونه تصاویر نسخه جدید و قدیمی پس از اجرای آزمایشی بصری ادغام می شوند.
شکل 2. جریان مقایسه بصری

مهار APIها برای مقایسه تصاویر

با در نظر گرفتن یک هدف روشن، زمان آن فرا رسیده بود که ببینیم Cypress چگونه می تواند به ما در دریافت اسکرین شات های مورد نیاز کمک کند. همانطور که گفته شد، ما تعداد زیادی تست رابط کاربری داشتیم که اکثر پورتال استعدادها را پوشش می‌داد، بنابراین در تلاش برای جمع‌آوری هرچه بیشتر مؤلفه‌های حیاتی، تصمیم گرفتیم پس از هر تعامل، از عناصر جداگانه اسکرین شات بگیریم.

یک رویکرد جایگزین، گرفتن اسکرین شات از کل صفحه در لحظات کلیدی در طول آزمایش بود، اما ما تصمیم گرفتیم که مقایسه این تصاویر بسیار دشوار باشد. همچنین، چنین مقایسه‌هایی می‌تواند بیشتر مستعد خطای انسانی باشد، مانند عدم تغییر پاورقی.

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

برای تولید تصاویر به Cypress’s API مراجعه کردیم. را cy.screenshot() فرمان می تواند در خارج از جعبه، تصاویر جداگانه ای از اجزا ایجاد کند، و بعد از Screenshot API به ما امکان می دهد نام فایل ها را تغییر دهیم، دایرکتوری ها را تغییر دهیم و اجراهای رگرسیون بصری را از نمونه های استاندارد تشخیص دهیم. با ترکیب این دو، اجراهایی ایجاد کردیم که بر تست های عملکردی ما تأثیری نداشت و ما را قادر ساخت که تصاویر را در پوشه های مناسب خود ذخیره کنیم.

ابتدا ما آن را تمدید کردیم index.js برای پشتیبانی از دو نوع اجرای جدید (پایه و مقایسه) در فهرست پلاگین های ما فایل کنید. سپس، مسیر تصاویر خود را با توجه به نوع اجرا تنظیم می کنیم:

// plugins/index.js
const fs = require('fs')
const path = require('path')
module.exports = (on, config) => {
// Adding these values to your config object allows you to access them in your tests.
  config.env.baseline = process.env.BASELINE || false
  config.env.comparison = process.env.COMPARISON || false

  on('after:screenshot', details => {
    // We only want to modify the behavior of baseline and comparison runs.
    if (config.env.baseline || config.env.comparison) {
      // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders.
      // An alternative would have been to look up the folder for the latest image, but this was the simpler approach.
      let lastScreenshotFile=""
      let lastScreenshotNumber = 0

      // We append the proper suffix number to the image, create the folder, and move the file.
      const createDirAndRename = filePath => {
        if (lastScreenshotFile === filePath) {
          lastScreenshotNumber++
        } else {
          lastScreenshotNumber = 0
        }
        lastScreenshotFile = filePath
        const newPath = filePath.replace(
          '.png',
          ` #${lastScreenshotNumber}.png`
        )

        return new Promise((resolve, reject) => {
          fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => {
            if (mkdirErr) {
              return reject(mkdirErr)
            }
            fs.rename(details.path, newPath, renameErr => {
              if (renameErr) {
                return reject(renameErr)
              }
              resolve({ path: newPath })
            })
          })
        })
      }

      const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}`

      return createDirAndRename(details.path
        .replace('cypress/integration', screenshotPath)
        .replace('All Specs', screenshotPath)
      )
    }
  })
  return config
}

سپس هر یک از اجراها را با افزودن متغیر محیطی مربوطه به فراخوانی Cypress در پروژه فراخوانی کردیم. package.json:

"scripts": {
  "cypress:baseline": "BASELINE=true yarn cypress:open",
  "cypress:comparison": "COMPARISON=true yarn cypress:open"
}

هنگامی که دستورات جدید خود را اجرا کردیم، می‌توانیم ببینیم که تمام اسکرین شات‌های گرفته شده در طول اجرا به پوشه‌های مناسب منتقل شده‌اند.

یک اسکرین شات که تصاویر گرفته شده در طول اجرا و انتقال به پوشه ها را نشان می دهد.
شکل 3. نتایج اجرای بصری

بعد، ما سعی کردیم رونویسی کنیم cy.get()، سرو دستور اصلی برای برگرداندن عناصر DOMو از هر عنصر فراخوانی شده همراه با اجرای پیش فرض آن اسکرین شات بگیرید. متاسفانه، cy.get() یک دستور پیچیده برای تغییر است، همانطور که دستور اصلی را در تعریف خودش فراخوانی می کند منجر به یک حلقه بی نهایت می شود. روش پیشنهادی برای حل این محدودیت این است که یک فرمان سفارشی جداگانه ایجاد کنید و سپس از آن فرمان جدید بخواهید پس از یافتن عنصر، یک اسکرین شات بگیرد:

Cypress.Commands.add("getAndScreenshot", (selector, options) => {
  // Note: You might need to tweak the command when getting multiple elements.
  return cy.get(selector).screenshot()
});

it("get overwrite", () => {
  cy.visit("
  cy.getAndScreenshot(".action-email")
})

با این حال، تماس های ما برای تعامل با عناصر موجود در صفحه قبلاً در یک داخلی پیچیده شده بود getElement() عملکرد. بنابراین تنها کاری که باید انجام می‌دادیم این بود که مطمئن شویم هنگام فراخوانی wrapper یک اسکرین شات گرفته شده است.

نتایج به دست آمده از طریق تست رگرسیون بصری

هنگامی که اسکرین شات ها را داشتیم، تنها کاری که باید انجام دهیم این بود که آنها را ادغام کنیم. برای آن، با استفاده از یک اسکریپت گره ساده ایجاد کردیم بوم. در پایان، اسکریپت ما را قادر ساخت 618 تصویر مقایسه تولید کنیم! با باز کردن پورتال استعدادهای درخشان، برخی از تفاوت‌ها به راحتی قابل تشخیص بود، اما برخی از مسائل چندان واضح نبودند.

مثال قبل و بعد از استفاده نادرست از پیکاسو، نشان دادن رنگ های قرمز و سیاه در عنصر.
شکل 4. مثالی از پیروی نکردن از دستورالعمل های جدید پیکاسو. یک تفاوت بود انتظار می رفت، اما نسخه جدید باید دارای پس زمینه قرمز و متن سفید باشد

مثال قبل و بعد از یک چیدمان اجزای کمی شکسته، نشان دادن متن ناهمتراز در کنار یک چک باکس در
شکل 5. نمونه ای از طرح بندی اجزای کمی شکسته

افزودن ارزش به تست UI

اول از همه، آزمایش‌های رگرسیون بصری اضافه شده مفید بودند و چند مسئله را که بدون آن‌ها می‌توانستیم از دست بدهیم، آشکار کردند. حتی اگر ما انتظار تفاوت در اجزای خود را داشتیم، دانستن اینکه چه چیزی واقعاً تغییر کرده است به محدود کردن موارد مشکل کمک کرد. بنابراین، اگر پروژه شما رابط کاربری دارد اما هنوز این تست ها را انجام نمی دهید، به آن برسید!

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

برای جزئیات بیشتر در مورد اجرای خط لوله رگرسیون بصری قوی در پروژه خود، لطفاً به صفحه تست بصری Cypress، ابزاری را انتخاب کنید که به بهترین وجه با نیازهای شما مطابقت دارد و فیلم های آموزشی را تماشا کنید.



منبع

Matthew Newman

Matthew Newman Matthew has over 15 years of experience in database management and software development, with a strong focus on full-stack web applications. He specializes in Django and Vue.js with expertise deploying to both server and serverless environments on AWS. He also works with relational databases and large datasets
[ Back To Top ]