ساختن نرم افزار قابل اعتماد: مفاهیم و تکنیک های آزمایش

Summarize this content to 400 words in Persian Lang
اطمینان از کیفیت و قابلیت اطمینان کد ضروری است! این اغلب به معنای استفاده از روشها و ابزارهای مختلف تست برای تأیید عملکرد نرمافزار مطابق انتظار است. بهعنوان توسعهدهندگان، بهویژه آنهایی که تازه وارد این حوزه شدهاند، درک مفاهیمی مانند تستهای واحد، ساختگیها، آزمایشهای سرتاسر، توسعه تست محور (TDD) و برخی دیگر از مفاهیمی که در این مقاله بیشتر به آنها خواهیم پرداخت، بسیار مهم است. هر یک از اینها نقش مهمی در اکوسیستم آزمایش ایفا می کند و به تیم ها کمک می کند تا برنامه های کاربردی قوی، قابل نگهداری و قابل اعتماد ایجاد کنند. هدف این است که برخی از تکنیک ها و مفاهیم تست، ارائه توضیحات و مثال های عملی را توضیح دهیم تا بتوان کمی بیشتر در مورد تست نرم افزار به خصوص در اکوسیستم جاوا اسکریپت فهمید.
تست های واحد
تستهای واحد جنبهای اساسی از تست نرمافزاری هستند که بر تأیید عملکرد اجزا یا واحدهای کد، معمولاً توابع یا روشها تمرکز دارند. هدف این تستها اطمینان از این است که هر واحد کد به صورت مجزا و بدون تکیه بر سیستمها یا وابستگیهای خارجی مطابق انتظار عمل میکند.
تست های واحد چیست؟
تست گرانول: تستهای واحد کوچکترین بخشهای یک برنامه کاربردی، مانند توابع یا روشها را هدف قرار میدهند.
انزوا: کد مورد آزمایش را از سایر بخش های برنامه و وابستگی های خارجی جدا می کنند.
خودکار: تستهای واحد معمولاً خودکار هستند و به آنها اجازه میدهند در طول توسعه مکررا اجرا شوند.
چرا از تست های واحد استفاده کنیم؟
تشخیص زودهنگام باگ: آنها در مراحل اولیه توسعه اشکالات را پیدا می کنند و رفع آنها را آسان تر و ارزان تر می کند.
کیفیت کد: تست های واحد با اطمینان از اینکه هر واحد کد به درستی کار می کند، کیفیت بهتر کد را ارتقا می دهد.
Refactoring Safety: آنها یک شبکه ایمنی را هنگام بازفرآوری کد ارائه می دهند و اطمینان می دهند که تغییرات باعث ایجاد اشکالات جدید نمی شوند.
مستندات: آزمونهای واحد به عنوان شکلی از مستندسازی عمل میکنند و نشان میدهند که واحدها چگونه باید عمل کنند.
سناریوی دنیای واقعی
سناریویی را در نظر بگیرید که در آن تابعی دارید که فاکتوریل یک عدد را محاسبه می کند. میخواهید مطمئن شوید که این عملکرد برای ورودیهای مختلف، از جمله موارد لبه، به درستی کار میکند.
کد مثال
در اینجا یک پیاده سازی ساده از یک تابع فاکتوریل و تست های واحد مربوط به آن با استفاده از Jest آورده شده است:
// factorial.js
function factorial(n) {
if (n 0) throw new Error(‘Negative input is not allowed’);
if (n === 0) return 1;
return n * factorial(n – 1);
}
module.exports = factorial;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست های واحد
// factorial.test.js
const factorial = require(‘./factorial’);
describe(‘Factorial Function’, () => {
it(‘should return 1 for input 0’, () => {
expect(factorial(0)).toBe(1);
});
it(‘should return 1 for input 1’, () => {
expect(factorial(1)).toBe(1);
});
it(‘should return 120 for input 5’, () => {
expect(factorial(5)).toBe(120);
});
it(‘should throw an error for negative input’, () => {
expect(() => factorial(-1)).toThrow(‘Negative input is not allowed’);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
پیاده سازی تابع: تابع فاکتوریل فاکتوریل یک عدد را به صورت بازگشتی با مدیریت خطا برای ورودی های منفی محاسبه می کند.
مجموعه تست: با استفاده از Jest، یک مجموعه تست (شرح) و چندین تست (it) برای ورودی های مختلف تعریف می کنیم.
موارد تست:• اولین آزمایش بررسی می کند که آیا تابع 1 را برای ورودی 0 برمی گرداند یا خیر.• تست دوم بررسی می کند که آیا تابع 1 را برای ورودی 1 برمی گرداند یا خیر.• تست سوم بررسی می کند که آیا تابع برای ورودی 5 عدد 120 را برمی گرداند یا خیر.• تست چهارم بررسی می کند که آیا تابع برای ورودی منفی خطا می دهد یا خیر.
مزایای آزمون های واحد
سرعت: تست های واحد سریع اجرا می شوند زیرا واحدهای کوچک کد را به صورت مجزا آزمایش می کنند.
قابلیت اطمینان: نتایج ثابتی را ارائه می دهند و به حفظ قابلیت اطمینان بالا در پایگاه های کد کمک می کنند.
پیشگیری از رگرسیون: با اجرای مکرر تست های واحد، توسعه دهندگان می توانند رگرسیون ها را در اوایل چرخه توسعه مشاهده کنند.
بهترین روش ها برای نوشتن تست های واحد
آزمون ها را کوچک و متمرکز نگه دارید: هر آزمون باید یک رفتار یا سناریوی خاص را تأیید کند.
از نام های توصیفی استفاده کنید: نام آزمون ها باید به وضوح آنچه را که آزمایش می کنند را توصیف کند.
از وابستگی های خارجی اجتناب کنید: وابستگی های خارجی را مسخره یا خرد کنید تا تست ها را ایزوله و سریع نگه دارید.
تست ها را به طور مکرر اجرا کنید: تست های واحد را در خط لوله ادغام پیوسته خود ادغام کنید تا آنها را در هر تغییر کد اجرا کنید.
کاور Edge Cases: مطمئن شوید که تست ها موارد لبه، از جمله شرایط خطا و مقادیر مرزی را پوشش می دهند.
ادغام تست های واحد در گردش کار توسعه شما می تواند به طور قابل توجهی قابلیت اطمینان و قابلیت نگهداری نرم افزار شما را افزایش دهد زیرا آنها بخش مهمی از یک استراتژی تست جامع هستند.
مسخره می کند
مسخره کردن یک مفهوم اساسی در تست نرم افزار است، به ویژه در هنگام برخورد با وابستگی هایی که تست را دشوار می کند. به بیان ساده، مسخره کردن است اشیایی که رفتار اشیاء واقعی را شبیه سازی می کنند به صورت کنترل شده این به شما این امکان را می دهد که کد خود را به صورت مجزا با جایگزین کردن وابستگی های واقعی با وابستگی های ساختگی آزمایش کنید.
چرا از Mocks استفاده کنیم؟
انزوا: کد خود را مستقل از سیستم ها یا سرویس های خارجی (مانند پایگاه داده ها، API ها و غیره) تست کنید.
سرعت: از تأخیر تماسهای شبکه یا عملیات پایگاه داده جلوگیری کنید تا آزمایشها سریعتر شود.
کنترل کنید: سناریوهای مختلف از جمله موارد لبه و شرایط خطا را شبیه سازی کنید، که ممکن است بازتولید آنها با وابستگی های واقعی دشوار باشد.
قابلیت اطمینان: اطمینان حاصل کنید که تست ها به طور مداوم بدون تاثیر محیط خارجی اجرا می شوند.
سناریوی دنیای واقعی
تصور کنید یک UserService دارید که باید کاربران جدیدی را با ذخیره اطلاعات آنها در پایگاه داده ایجاد کند. در طول آزمایش، به دلایل مختلف (سرعت، هزینه، یکپارچگی داده ها) نمی خواهید عملاً عملیات پایگاه داده را انجام دهید. در عوض، شما از یک ماک برای شبیه سازی تعامل پایگاه داده استفاده می کنید. با استفاده از یک mock، می توانید اطمینان حاصل کنید که متد saveUser هنگام اجرای createUser به درستی فراخوانی می شود.
بیایید این سناریو را با استفاده از Node.js با یک راهاندازی آزمایشی شامل Jest برای تمسخر کلاس پایگاه داده و تأیید تعاملات در UserService بررسی کنیم.
کد مثال
UserService.ts
export class UserService {
private db;
constructor(db) {
this.db = db;
}
getUser(id: string) {
return this.db.findUserById(id);
}
createUser(user) {
this.db.saveUser(user);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پایگاه داده.ts
export class Database {
findUserById(id: string) {
// Simulate database lookup
return { id, name: “John Doe” };
}
saveUser(user) {
// Simulate saving user to the database
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست با Mock
import { UserService } from ‘./UserService’;
import { Database } from ‘./Database’;
jest.mock(‘./Database’);
describe(‘UserService – Mocks’, () => {
let userService;
let mockDatabase;
beforeEach(() => {
mockDatabase = new Database();
userService = new UserService(mockDatabase);
});
it(‘should call saveUser when createUser is called’, () => {
const user = { id: ‘123’, name: ‘Alice’ };
userService.createUser(user);
expect(mockDatabase.saveUser).toHaveBeenCalled();
expect(mockDatabase.saveUser).toHaveBeenCalledWith(user);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
راه اندازی Mocks: قبل از هر تست، ما یک mock برای کلاس Database با استفاده از Jest برای شبیه سازی رفتار متد saveUser تنظیم می کنیم.
تعریف رفتار: ما اطمینان میدهیم که mockDatabase.saveUser با شیء کاربر صحیح هنگام اجرای createUser فراخوانی میشود.
**مورد تست: **ما بررسی می کنیم که createUser به درستی saveUser را با جزئیات کاربر ارائه شده فراخوانی می کند.
با استفاده از ماک ها، ما UserService را از پایگاه داده واقعی جدا می کنیم و محیط آزمایش را کنترل می کنیم، و مطمئن می شویم که تست های ما قابل اعتماد و کارآمد هستند. این رویکرد در بسیاری از زبان های برنامه نویسی و چارچوب های آزمایشی رایج است و آن را به یک مفهوم جهانی در توسعه نرم افزار تبدیل می کند.
خرد
Stubs، مانند mock ها، دو تست هستند که برای شبیه سازی رفتار اشیاء واقعی به روشی کنترل شده در طول آزمایش استفاده می شوند. با این حال، برخی از تفاوت های کلیدی بین خرد و مسخره وجود دارد.
خرد چیست؟
خرد هستند پاسخ های از پیش تعریف شده به تماس های خاص در طول آزمون ساخته شده است. بر خلاف ماکها، که میتوانند برای تأیید تعاملات و رفتارها (مانند اطمینان از فراخوانی روشهای خاص) نیز مورد استفاده قرار گیرند، استابها عمدتاً بر ارائه خروجیهای کنترلشده برای فراخوانیهای متد متمرکز هستند.
چرا از Stubs استفاده کنیم؟
کنترل کنید: پاسخهای از پیش تعیینشده را به فراخوانیهای متد ارائه دهید، و از نتایج آزمون ثابت اطمینان حاصل کنید.
انزوا: کد مورد آزمایش را از وابستگی های خارجی، شبیه به ماک ها جدا کنید.
سادگی: تنظیم و استفاده در زمانی که فقط نیاز به کنترل مقادیر برگشتی دارید و تعاملات را تأیید نمی کنید، اغلب ساده تر است.
سناریوی دنیای واقعی
سناریویی را در نظر بگیرید که در آن سرویسی دارید که قیمت کل اقلام موجود در سبد خرید را محاسبه می کند. این سرویس برای دریافت قیمت هر کالا به سرویس دیگری متکی است. در طول آزمایش، شما نمی خواهید به خدمات واقعی واکشی قیمت تکیه کنید، بنابراین از خرد برای شبیه سازی رفتار استفاده می کنید.
کد مثال
پیاده سازی خدمات
// cartService.js
class CartService {
constructor(priceService) {
this.priceService = priceService;
}
async calculateTotal(cart) {
let total = 0;
for (let item of cart) {
const price = await this.priceService.getPrice(item.id);
total += price * item.quantity;
}
return total;
}
}
module.exports = CartService;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست با Stubs
// cartService.test.js
const chai = require(‘chai’);
const sinon = require(‘sinon’);
const CartService = require(‘./cartService’);
const expect = chai.expect;
describe(‘CartService’, () => {
let priceServiceStub;
let cartService;
beforeEach(() => {
priceServiceStub = {
getPrice: sinon.stub()
};
cartService = new CartService(priceServiceStub);
});
it(‘should calculate the total price of items in the cart’, async () => {
priceServiceStub.getPrice.withArgs(1).resolves(10);
priceServiceStub.getPrice.withArgs(2).resolves(20);
const cart = [
{ id: 1, quantity: 2 },
{ id: 2, quantity: 1 }
];
const total = await cartService.calculateTotal(cart);
expect(total).to.equal(40);
});
it(‘should handle an empty cart’, async () => {
const cart = [];
const total = await cartService.calculateTotal(cart);
expect(total).to.equal(0);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
راه اندازی خرد: قبل از هر آزمایش، ما priceServiceStub را با استفاده از Sinon ایجاد میکنیم تا روش getPrice را خرد کنیم.
رفتار را تعریف کنید: ما رفتار PriceServiceStub را برای ورودی های خاص تعریف می کنیم:• withArgs(1).resolves(10) getPrice(1) را 10 برمی گرداند.•withArgs(2).resolves(20) getPrice(2) را 20 برمی گرداند.
موارد تست:• در اولین آزمایش، تأیید می کنیم که CalculTotal به درستی قیمت کل اقلام موجود در سبد خرید را محاسبه می کند.• در تست دوم، بررسی میکنیم که در صورت خالی بودن سبد،calculTotal 0 برمیگردد.
با خردهها، CartService را از خدمات واکشی قیمت واقعی جدا میکنیم و مقادیر بازگشتی کنترلشده را ارائه میکنیم، و از نتایج آزمایشی ثابت و قابل اعتماد اطمینان میدهیم. Stubs زمانی مفید است که شما نیاز به کنترل مقادیر برگشتی متدها بدون تأیید برهمکنش بین اشیا دارید، که آنها را جایگزین سادهتری برای mock در بسیاری از سناریوها میکند.
جاسوس ها
جاسوس ها نوع دیگری از تست های دوگانه هستند که در تست واحد برای مشاهده رفتار توابع استفاده می شوند. برخلاف تمسخر و خرد، جاسوس ها در درجه اول استفاده می شوند نظارت بر نحوه فراخوانی توابع در طول اجرای آزمون آنها میتوانند توابع یا روشهای موجود را بپیچند و به شما این امکان را میدهند که بررسی کنید که آیا و چگونه فراخوانی شدهاند، بدون اینکه لزوماً رفتار آنها را تغییر دهید.
جاسوس ها چیست؟
جاسوس ها برای موارد زیر استفاده می شوند:
پیگیری تماسهای تابع: بررسی کنید که آیا یک تابع فراخوانی شده است، چند بار و با چه آرگومان هایی فراخوانی شده است.
نظارت بر تعاملات: تعامل بین قسمت های مختلف کد را مشاهده کنید.
بررسی عوارض جانبی: اطمینان حاصل کنید که برخی از توابع به عنوان بخشی از اجرای کد فراخوانی شده اند.
چرا از جاسوس استفاده کنیم؟
غیر مزاحم: جاسوسها میتوانند روشهای موجود را بدون تغییر رفتارشان بپیچانند و باعث میشوند که آنها کمتر مداخله کنند.
تأیید: برای تأیید اینکه برخی روش ها یا توابع به درستی در طول آزمایش ها فراخوانی می شوند عالی است.
انعطاف پذیری: می تواند همراه با خرد و ماک برای تست جامع استفاده شود.
سناریوی دنیای واقعی
تصور کنید یک NotificationService دارید که اعلانها را ارسال میکند و این اقدامات را ثبت میکند. شما می خواهید مطمئن شوید که هر بار که اعلان ارسال می شود، به درستی ثبت شده است. به جای جایگزینی عملکرد گزارش، می توانید از یک جاسوس برای نظارت بر تماس های روش گزارش استفاده کنید.
کد مثال
NotificationService.ts
export class NotificationService {
private logger;
constructor(logger) {
this.logger = logger;
}
sendNotification(message: string) {
// Simulate sending a notification
this.logger.log(`Notification sent: ${message}`);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
Logger.ts
export class Logger {
log(message: string) {
console.log(message);
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست با جاسوس
import { NotificationService } from ‘./NotificationService’;
import { Logger } from ‘./Logger’;
import { jest } from ‘@jest/globals’;
describe(‘NotificationService – Spies’, () => {
let notificationService;
let logger;
beforeEach(() => {
logger = new Logger();
notificationService = new NotificationService(logger);
});
it(‘should call log method when sendNotification is called’, () => {
const logSpy = jest.spyOn(logger, ‘log’);
const message = ‘Hello, World!’;
notificationService.sendNotification(message);
expect(logSpy).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith(`Notification sent: ${message}`);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
سرویس راه اندازی: قبل از هر آزمایش، ما یک notificationService با استفاده از یک نمونه لاگر ایجاد می کنیم تا از روش log جاسوسی کنیم.
فراخوانی روش ها: روش sendNotification را در نمونه notificationService با یک پیام آزمایشی فراخوانی می کنیم.
تأیید تماس ها: بررسی می کنیم که هنگام ارسال اعلان، متد log و با آرگومان صحیح فراخوانی شود.
Spies به ما امکان می دهد بدون تغییر رفتار آن، تأیید کنیم که متد log همانطور که انتظار می رود فراخوانی می شود. جاسوس ها به ویژه برای تأیید تعاملات و عوارض جانبی در کد شما مفید هستند و آنها را به ابزاری ارزشمند برای اطمینان از صحت رفتار برنامه شما در طول آزمایش تبدیل می کند.
تست های یکپارچه سازی
تستهای یکپارچهسازی برای تأیید اینکه ماژولهای مختلف یک برنامه نرمافزاری همانطور که انتظار میرود تعامل دارند، مهم هستند. برخلاف تستهای واحد که بر واحدهای تک کد تمرکز میکنند، تستهای یکپارچهسازی همکاری بین اجزای یکپارچه را ارزیابی میکنند و هر مشکلی را که ممکن است از عملکرد ترکیبی آنها ناشی شود، شناسایی میکنند.
تست های یکپارچه سازی چیست؟
اجزای ترکیبی: تست های یکپارچه سازی میزان کارکرد قطعات ترکیب شده یک سیستم را ارزیابی می کنند.
محیط واقعی: این تست ها اغلب از سناریوهای واقعی تری در مقایسه با تست های واحد، شامل پایگاه های داده، API های خارجی و سایر اجزای سیستم استفاده می کنند.
تست میان افزار: میان افزار و اتصالات بین قسمت های مختلف سیستم را تست می کنند.
چرا از تست های یکپارچه سازی استفاده کنیم؟
شناسایی مشکلات رابط: آنها به شناسایی مسائل در مرزهایی که اجزای مختلف با هم تعامل دارند کمک می کنند.
از هم افزایی اجزا اطمینان حاصل کنید: بررسی کنید که قسمت های مختلف سیستم همانطور که انتظار می رود با هم کار می کنند.
قابلیت اطمینان سیستم: قابلیت اطمینان کلی سیستم را با تشخیص خطاهایی که ممکن است در تست های واحد از دست برود، افزایش دهید.
سناریوهای پیچیده: سناریوهای پیچیده تر و دنیای واقعی را که شامل چندین بخش از سیستم می شوند، آزمایش کنید.
سناریوی دنیای واقعی
یک برنامه وب با یک API پشتیبان و یک پایگاه داده را در نظر بگیرید. شما می خواهید اطمینان حاصل کنید که یک نقطه پایانی API خاص به درستی داده ها را از پایگاه داده بازیابی می کند و آنها را در قالب مورد انتظار برمی گرداند.
کد مثال
در اینجا یک مثال ساده از یک تست یکپارچه سازی برای یک برنامه Node.js با استفاده از Jest و Supertest آورده شده است:
// app.js
const express = require(‘express’);
const app = express();
const { getUser } = require(‘./database’);
app.get(‘/user/:id’, async (req, res) => {
try {
const user = await getUser(req.params.id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).send(‘User not found’);
}
} catch (error) {
res.status(500).send(‘Server error’);
}
});
module.exports = app;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست های یکپارچه سازی
// app.test.js
const request = require(‘supertest’);
const app = require(‘./app’);
const { getUser } = require(‘./database’);
jest.mock(‘./database’);
describe(‘GET /user/:id’, () => {
it(‘should return a user for a valid ID’, async () => {
const userId = ‘1’;
const user = { id: ‘1’, name: ‘John Doe’ };
getUser.mockResolvedValue(user);
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(200);
expect(response.body).toEqual(user);
});
it(‘should return 404 if user is not found’, async () => {
const userId = ‘2’;
getUser.mockResolvedValue(null);
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(404);
expect(response.text).toBe(‘User not found’);
});
it(‘should return 500 on server error’, async () => {
const userId = ‘3’;
getUser.mockRejectedValue(new Error(‘Database error’));
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(500);
expect(response.text).toBe(‘Server error’);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
راه اندازی برنامه: برنامه Express مسیری را تعریف می کند که کاربر را با شناسه از پایگاه داده دریافت می کند.
تمسخر وابستگی ها: تابع getUser از ماژول پایگاه داده برای شبیه سازی سناریوهای مختلف مسخره می شود.
مجموعه تست:• اولین آزمایش بررسی می کند که آیا نقطه پایانی داده های کاربر صحیح را برای یک شناسه معتبر برمی گرداند یا خیر.• تست دوم بررسی می کند که اگر کاربر پیدا نشود وضعیت 404 برگردانده می شود.• تست سوم تضمین می کند که در صورت خطای سرور، وضعیت 500 برگردانده می شود.
مزایای تست های یکپارچه سازی
پوشش جامع: آنها با اعتبارسنجی تعاملات بین مؤلفه های متعدد، پوشش آزمون جامع تری را ارائه می دهند.
شناسایی مسائل پنهان: اشکالاتی را که ممکن است هنگام آزمایش اجزا به صورت مجزا آشکار نشوند، پیدا کنید.
افزایش اعتماد به نفس: افزایش اطمینان در عملکرد و قابلیت اطمینان کلی سیستم.
سناریوهای دنیای واقعی: سناریوهایی را آزمایش کنید که از نزدیک استفاده در دنیای واقعی برنامه را تقلید می کنند.
بهترین روش ها برای نوشتن تست های یکپارچه سازی
محیط های واقع گرایانه: از محیط هایی که شباهت زیادی به تولید دارند برای کشف مسائل خاص محیط استفاده کنید.
مدیریت داده ها: برای اطمینان از اجرای آزمایشها با حالتهای شناخته شده و قابل پیشبینی، دادههای آزمایش را تنظیم و از بین ببرید.
خدمات خارجی ساختگی: وابستگی ها و سرویس های خارجی را مسخره کنید تا روی یکپارچگی بین اجزای خود تمرکز کنید.
تست تعاملات کلیدی: روی آزمایش مسیرهای حیاتی و تعاملات کلیدی بین اجزای سیستم تمرکز کنید.
با تست های واحد ترکیب شود: از تست های یکپارچه سازی همراه با تست های واحد برای پوشش کامل استفاده کنید.
تست های پایان به انتها
تستهای End-to-End (E2E) نوعی آزمایش هستند که بر تأیید عملکرد کامل یک برنامه تمرکز میکنند و اطمینان حاصل میکنند که از ابتدا تا انتها طبق برنامه کار میکند. بر خلاف تست های واحد که اجزا یا عملکردهای جداگانه را آزمایش می کنند، تست های E2E تعاملات واقعی کاربر را شبیه سازی کنید و کل سیستم را آزمایش کنید، از جمله قسمت جلو، باطن و پایگاه داده.
تست های End-to-End چیست؟
گردش کار کامل را تست کنید: آنها جریان کامل برنامه را از رابط کاربری (UI) به باطن و برگشت آزمایش می کنند.
شبیه سازی اقدامات کاربر واقعی: آنها تعاملات کاربر مانند کلیک کردن روی دکمه ها، پر کردن فرم ها و پیمایش در برنامه را شبیه سازی می کنند.
اطمینان از یکپارچگی: آنها بررسی می کنند که تمام قسمت های سیستم به درستی با هم کار می کنند.
چرا از تست های End-to-End استفاده کنیم؟
پوشش جامع: آنها بالاترین سطح اطمینان را از عملکرد برنامه به طور کلی ارائه می دهند.
مسائل ادغام را بگیرید: آنها مشکلاتی را که هنگام تعامل بخش های مختلف سیستم رخ می دهد شناسایی می کنند.
کاربر محور: آنها تأیید می کنند که برنامه از دیدگاه کاربر به درستی رفتار می کند.
سناریوی دنیای واقعی
بیایید سناریویی را در نظر بگیریم که در آن شما یک برنامه وب با قابلیت ورود کاربر دارید. یک تست E2E برای این سناریو شامل باز کردن صفحه ورود، وارد کردن نام کاربری و رمز عبور، کلیک کردن روی دکمه ورود به سیستم و تأیید اینکه کاربر با موفقیت وارد شده و به داشبورد هدایت شده است، میشود.
ما یک تست ساده E2E با استفاده از موکا، چای و سوپرتست برای آزمایش یک برنامه کاربردی Node.js ایجاد خواهیم کرد.
کد مثال
پیاده سازی مسیر باطن
// app.js
const express = require(‘express’);
const bodyParser = require(‘body-parser’);
const app = express();
app.use(bodyParser.json());
app.post(‘/login’, (req, res) => {
const { username, password } = req.body;
if (username === ‘john_doe’ && password === ‘password123’) {
return res.status(200).send({ message: ‘Welcome, John Doe’ });
}
return res.status(401).send({ message: ‘Invalid credentials’ });
});
app.get(‘/dashboard’, (req, res) => {
res.status(200).send({ message: ‘This is the dashboard’ });
});
module.exports = app;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست E2E با موکا، چای و سوپرتست
// app.test.js
const chai = require(‘chai’);
const chaiHttp = require(‘chai-http’);
const app = require(‘./app’);
const expect = chai.expect;
chai.use(chaiHttp);
describe(‘User Login E2E Test’, () => {
it(‘should log in and redirect to the dashboard’, (done) => {
chai.request(app)
.post(‘/login’)
.send({ username: ‘john_doe’, password: ‘password123’ })
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.property(‘message’, ‘Welcome, John Doe’);
chai.request(app)
.get(‘/dashboard’)
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.property(‘message’, ‘This is the dashboard’);
done();
});
});
});
it(‘should not log in with invalid credentials’, (done) => {
chai.request(app)
.post(‘/login’)
.send({ username: ‘john_doe’, password: ‘wrongpassword’ })
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body).to.have.property(‘message’, ‘Invalid credentials’);
done();
});
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
توضیح
تست راه اندازی: ما یک مجموعه آزمایشی را با استفاده از موکا و چای توصیف می کنیم (شرح) و موارد آزمایشی (آن را).
شبیه سازی اقدامات کاربر:• ما از chai.request(app) برای شبیه سازی درخواست های HTTP به برنامه استفاده می کنیم.• .post('/login') یک درخواست POST را با نام کاربری و رمز عبور به نقطه پایانی ورود ارسال می کند.• .send({ username: 'john_doe', password: 'password123' }) اعتبار ورود به سیستم را ارسال می کند.
بررسی نتایج:• وضعیت پاسخ و پیام را بررسی می کنیم تا ورود موفقیت آمیز را تأیید کنیم.• ما یک درخواست GET را به نقطه پایانی داشبورد ارسال می کنیم و پاسخ را بررسی می کنیم تا پس از ورود به سیستم، دسترسی به داشبورد را تأیید کنیم.
رسیدگی به خطاها: ما همچنین سناریویی را که در آن اعتبارنامه های ورود نامعتبر است آزمایش می کنیم و پیام خطا و کد وضعیت مناسب را تأیید می کنیم.
مزایای تست E2E
اعتبار سنجی تجربه کاربر: تست های E2E تضمین می کند که برنامه تجربه کاربری خوبی را ارائه می دهد.
تست جامع: آنها کل پشته برنامه را آزمایش میکنند و مشکلاتی را که ممکن است تستهای واحد یا یکپارچهسازی از دست بدهند، تشخیص میدهند.
اتوماسیون: تستهای E2E را میتوان خودکار کرد و به شما این امکان را میدهد که آنها را به عنوان بخشی از خط لوله CI/CD خود اجرا کنید تا مشکلات را قبل از استقرار پیدا کنید.
تست های End-to-End برای تایید عملکرد کامل و تجربه کاربری برنامه شما بسیار مهم هستند. آنها **تضمین می کنند که تمام قسمت های سیستم شما با هم کار می کنند **و سناریوهای کاربر دنیای واقعی به درستی مدیریت می شوند. با استفاده از ابزارهایی مانند Mocha، Chai و Supertest، میتوانید این تستها را خودکار کنید تا اطمینان بالایی نسبت به کیفیت و قابلیت اطمینان برنامه خود داشته باشید.
پوشش کد
پوشش کد معیاری است که در تست نرم افزار برای اندازه گیری میزان اجرای کد منبع یک برنامه هنگام اجرای یک مجموعه آزمایشی خاص استفاده می شود. کمک می کند تعیین کنید چه مقدار از کد شما در حال آزمایش است و می تواند مناطقی از پایگاه کد را شناسایی کند که تحت هیچ آزمایشی قرار نگرفته اند.
مفاهیم کلیدی پوشش کد
پوشش بیانیه: درصد دستورات اجرایی را که اجرا شده اند اندازه گیری می کند.
پوشش شعبه: درصد شعب (نقاط تصمیم گیری مانند شرایط if-else) را که اجرا شده اند را اندازه گیری می کند.
پوشش عملکرد: درصد توابع یا متدهایی که فراخوانی شده اند را اندازه گیری می کند.
پوشش خط: درصد خطوط کد اجرا شده را اندازه گیری می کند.
پوشش شرایط: درصد عبارات فرعی بولی را در شرایط شرطی که هم درست و هم نادرست ارزیابی شده اند را اندازه گیری می کند.
چرا از پوشش کد استفاده کنیم؟
کد تست نشده را شناسایی کنید: به شما کمک می کند قسمت هایی از پایگاه کد خود را پیدا کنید که تحت آزمایش نیستند.
بهبود کیفیت تست: اطمینان حاصل می کند که آزمایشات شما کامل است و سناریوهای مختلف را پوشش می دهد.
کیفیت کد را حفظ کنید: با تشویق تست های جامع تر، شیوه های نگهداری بهتر کد را ترویج می کند.
اشکالات را کاهش دهید: با اطمینان از تست بیشتر کد شما، احتمال ابتلا به اشکالات و خطاها را افزایش می دهد.
نمونه ای از پوشش کد
یک تابع ساده و تست های آن را در نظر بگیرید:
پیاده سازی
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function subtract(a, b) {
return a – b;
}
module.exports = { add, multiply, subtract };
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست ها
// math.test.js
const chai = require(‘chai’);
const expect = chai.expect;
const { add, multiply, subtract } = require(‘./math’);
describe(‘Math Functions’, () => {
it(‘should add two numbers’, () => {
expect(add(2, 3)).to.equal(5);
});
it(‘should multiply two numbers’, () => {
expect(multiply(2, 3)).to.equal(6);
});
it(‘should subtract two numbers’, () => {
expect(subtract(5, 3)).to.equal(2);
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد پوشش کد
برای اندازه گیری پوشش کد، می توانیم از ابزاری مانند استانبول (که اکنون NYC نامیده می شود) استفاده کنیم. در اینجا نحوه تنظیم آن آمده است:
NYC را نصب کنید: ابتدا، NYC را به عنوان وابستگی توسعه نصب کنید.
npm install –save-dev nyc
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
NYC را پیکربندی کنید: یک پیکربندی در package.json خود اضافه کنید یا یک فایل .nycrc ایجاد کنید.
// package.json
“nyc”: {
“reporter”: [“html”, “text”],
“exclude”: [“test”]
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست ها را با پوشش اجرا کنید: اسکریپت آزمایشی خود را طوری تغییر دهید که شامل NYC باشد.
// package.json
“scripts”: {
“test”: “nyc mocha”
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست ها را اجرا کنید: تست های خود را با دستور coverage اجرا کنید.
npm test
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تفسیر گزارش های پوشش کد
پس از اجرای آزمایشها، NYC یک گزارش پوشش ایجاد میکند. این گزارش معمولاً شامل:
خلاصه: خلاصه ای از درصدهای پوشش برای دستورات، شاخه ها، توابع و خطوط.
گزارش تفصیلی: گزارش مفصلی که نشان می دهد کدام خطوط کد پوشش داده شده است و کدام نه.
خروجی نمونه (ساده شده):
=============================== Coverage summary ===============================
Statements : 100% ( 12/12 )
Branches : 100% ( 4/4 )
Functions : 100% ( 3/3 )
Lines : 100% ( 12/12 )
================================================================================
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پوشش کد یک معیار خوب در تست نرم افزار است که به شما کمک می کند تا مطمئن شوید کد شما به خوبی تست شده و قابل اعتماد است. با استفاده از ابزارهایی مانند NYC، می توانید اندازه گیری و تجسم کنید که چه مقدار از کد شما توسط آزمایش ها پوشش داده شده است، شکاف ها را شناسایی کرده و کیفیت کلی پایگاه کد خود را بهبود بخشید. پوشش کد بالا می تواند به طور قابل توجهی خطر اشکالات را کاهش دهد و قابلیت نگهداری نرم افزار شما را بهبود بخشد.
توسعه آزمایش محور
توسعه تست محور (TDD) یک رویکرد توسعه نرم افزار است که در آن تست ها قبل از کد واقعی نوشته می شوند. این فرآیند ابتدا بر نوشتن یک آزمون مردود، سپس نوشتن حداقل کد مورد نیاز برای قبولی در آن آزمون، و در نهایت تغییر کد برای مطابقت با استانداردهای قابل قبول تأکید دارد. هدف TDD این است که اطمینان حاصل کند که کد قابل اعتماد، قابل نگهداری است و از ابتدا الزامات را برآورده می کند.
مفاهیم کلیدی توسعه آزمایش محور
چرخه قرمز-سبز-رفاکتور:• قرمز: یک آزمایش برای یک عملکرد یا ویژگی جدید بنویسید. در ابتدا، آزمایش ناموفق خواهد بود زیرا این ویژگی هنوز اجرا نشده است.• سبز: حداقل کد لازم برای قبولی در آزمون را بنویسید.• Refactor: کد جدید را اصلاح کنید تا ساختار و خوانایی آن بدون تغییر رفتار بهبود یابد. اطمینان حاصل کنید که همه آزمایشها پس از فاکتورسازی مجدد همچنان انجام میشوند.
تکرارهای کوچک: TDD تغییرات کوچک و تدریجی را تشویق می کند. هر تکرار شامل نوشتن یک تست، قبولی آن و سپس بازآفرینی است.
روی الزامات تمرکز کنید: تست های نوشتاری ابتدا توسعه دهندگان را مجبور می کند تا قبل از اجرا، الزامات و طراحی ویژگی را در نظر بگیرند.
چرا از توسعه تست محور استفاده کنیم؟
بهبود کیفیت کد: TDD منجر به کدهایی با طراحی بهتر، تمیزتر و قابل نگهداری تر می شود.
اشکال زدایی کمتر: اشکالات در مراحل اولیه توسعه یافت می شوند و زمان صرف شده برای اشکال زدایی را کاهش می دهند.
درک بهتر نیازها: نوشتن تست ها ابتدا به روشن شدن الزامات و طراحی قبل از اجرا کمک می کند.
پوشش تست بالا: از آنجایی که تست ها برای هر ویژگی نوشته می شوند، TDD پوشش کد بالایی را تضمین می کند.
نمونه ای از توسعه تست محور
بیایید یک مثال ساده از پیادهسازی یک تابع را برای بررسی اول بودن عدد با استفاده از TDD در جاوا اسکریپت با Mocha و Chai مرور کنیم.
مرحله 1: نوشتن یک آزمون ناموفق (قرمز)
ابتدا یک تست برای تابع isPrime می نویسیم که هنوز وجود ندارد.
// isPrime.test.js
const chai = require(‘chai’);
const expect = chai.expect;
const { isPrime } = require(‘./isPrime’);
describe(‘isPrime’, () => {
it(‘should return true for prime number 7’, () => {
expect(isPrime(7)).to.be.true;
});
it(‘should return false for non-prime number 4’, () => {
expect(isPrime(4)).to.be.false;
});
it(‘should return false for number 1’, () => {
expect(isPrime(1)).to.be.false;
});
it(‘should return false for number 0’, () => {
expect(isPrime(0)).to.be.false;
});
it(‘should return false for negative numbers’, () => {
expect(isPrime(-3)).to.be.false;
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست را اجرا کنید، زیرا تابع isPrime هنوز تعریف نشده است.
مرحله 2: نوشتن کد حداقلی برای قبولی در آزمون (سبز)
سپس حداقل کد مورد نیاز برای قبولی در آزمون را می نویسیم.
// isPrime.js
function isPrime(num) {
if (num 1) return false;
for (let i = 2; i num; i++) {
if (num % i === 0) return false;
}
return true;
}
module.exports = { isPrime };
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دوباره تست را اجرا کنید. این بار باید بگذرد
مرحله 3: کد را اصلاح کنید
در نهایت، ما کد را بازسازی می کنیم تا کارایی و خوانایی آن را بدون تغییر رفتار آن بهبود ببخشیم.
// isPrime.js
function isPrime(num) {
if (num 1) return false;
if (num 3) return true;
if (num % 2 === 0 || num % 3 === 0) return false;
for (let i = 5; i * i num; i += 6) {
if (num % i === 0 || num % (i + 2) === 0) return false;
}
return true;
}
module.exports = { isPrime };
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تست ها را دوباره اجرا کنید تا مطمئن شوید که همه آنها هنوز پس از بازآفرینی موفق هستند.
مزایای TDD
اعتماد به کد: اطمینان حاصل می کند که تغییرات کد باگ جدیدی ایجاد نمی کند.
مستندات: تست ها به عنوان شکلی از مستندات عمل می کنند و نمونه هایی از نحوه عملکرد کد را ارائه می دهند.
بهبودهای طراحی: طراحی و معماری بهتر نرم افزار را تشویق می کند.
کاهش زمان رفع اشکال: تشخیص زودهنگام اشکال زمان صرف شده برای اشکال زدایی را به حداقل می رساند.
Test Driven Development (TDD) یک متدولوژی قدرتمند است که به توسعه دهندگان کمک می کند کدی با کیفیت بالا، قابل اعتماد و قابل نگهداری ایجاد کنند. با پیروی از چرخه Red-Green-Refactor، توسعه دهندگان می توانند اطمینان حاصل کنند که کد آنها از ابتدا الزامات را برآورده می کند و پوشش تست بالایی را حفظ می کند. TDD منجر به طراحی بهتر، اشکال زدایی کمتر و اطمینان بیشتر در پایگاه کد می شود.
توسعه رفتار محور
توسعه مبتنی بر رفتار (BDD) یک متدولوژی توسعه نرم افزار است که اصول توسعه آزمایش محور (TDD) را با تمرکز بر روی رفتار سیستم از دیدگاه ذینفعان آن. BDD بر همکاری بین توسعه دهندگان، آزمایش کنندگان و ذینفعان تجاری تاکید می کند تا اطمینان حاصل شود که نرم افزار رفتارها و الزامات مورد نظر را برآورده می کند.
مفاهیم کلیدی توسعه رفتار محور
تفاهم مشترک: BDD همکاری و ارتباط بین اعضای تیم را تشویق می کند تا اطمینان حاصل شود که همه درک روشنی از رفتار مطلوب سیستم دارند.
داستان ها و سناریوهای کاربر: BDD از داستان ها و سناریوهای کاربر برای توصیف رفتار مورد انتظار سیستم به زبان ساده استفاده می کند که هم برای ذینفعان فنی و هم غیر فنی قابل درک باشد.
نحو داده شده-وقتی-پس: سناریوهای BDD معمولاً از یک قالب ساختاریافته به نام Given-When-Then پیروی می کنند که زمینه اولیه (Given)، اقدام در حال انجام (When) و نتیجه مورد انتظار (Then) را توصیف می کند.
آزمون های پذیرش خودکارسناریوهای BDD اغلب با استفاده از چارچوبهای آزمایشی خودکار میشوند و به آنها اجازه میدهند هم به عنوان مشخصات اجرایی و هم بهعنوان تست رگرسیون عمل کنند.
چرا از توسعه مبتنی بر رفتار استفاده کنیم؟
وضوح و درک: BDD درک مشترک نیازها را در بین اعضای تیم ترویج می کند و ابهامات و سوء تفاهم ها را کاهش می دهد.
همسویی با اهداف تجاری: با تمرکز بر رفتارها و داستان های کاربر، BDD تضمین می کند که تلاش های توسعه با اهداف تجاری و نیازهای کاربر همسو هستند.
تشخیص زودهنگام مسائل: سناریوهای BDD به عنوان معیارهای پذیرش اولیه عمل میکنند و به تیمها اجازه میدهند مسائل و سوء تفاهمها را در مراحل اولیه توسعه شناسایی کنند.
همکاری بهبود یافته: BDD همکاری بین توسعهدهندگان، آزمایشکنندگان و ذینفعان کسبوکار را تشویق میکند و یک احساس مشترک مالکیت و مسئولیت نسبت به کیفیت نرمافزار را تقویت میکند.
نمونه ای از توسعه رفتار محور
بیایید یک مثال ساده از پیاده سازی یک ویژگی برای برداشت پول از ATM با استفاده از BDD با نحو Gherkin و Cucumber.js را در نظر بگیریم.
فایل ویژگی (ATMWithdrawal.feature)
Feature: ATM Withdrawal
As a bank customer
I want to withdraw money from an ATM
So that I can access my funds
Scenario: Withdrawal with sufficient balance
Given my account has a balance of $100
When I withdraw $20 from the ATM
Then the ATM should dispense $20
And my account balance should be $80
Scenario: Withdrawal with insufficient balance
Given my account has a balance of $10
When I withdraw $20 from the ATM
Then the ATM should display an error message
And my account balance should remain $10
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تعاریف مرحله (atmWithdrawal.js)
const { Given, When, Then } = require(‘cucumber’);
const { expect } = require(‘chai’);
let accountBalance = 0;
let atmBalance = 100;
Given(‘my account has a balance of ${int}’, function (balance) {
accountBalance = balance;
});
When(‘I withdraw ${int} from the ATM’, function (amount) {
if (amount > accountBalance) {
this.errorMessage = ‘Insufficient funds’;
return;
}
accountBalance -= amount;
atmBalance -= amount;
this.withdrawnAmount = amount;
});
Then(‘the ATM should dispense ${int}’, function (amount) {
expect(this.withdrawnAmount).to.equal(amount);
});
Then(‘my account balance should be ${int}’, function (balance) {
expect(accountBalance).to.equal(balance);
});
Then(‘the ATM should display an error message’, function () {
expect(this.errorMessage).to.equal(‘Insufficient funds’);
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اجرای سناریوها
میتوانید سناریوها را با استفاده از Cucumber.js اجرا کنید، که فایل ویژگی را تجزیه میکند، مراحل را با تعاریف آنها مطابقت میدهد و آزمایشها را اجرا میکند.
مزایای BDD
تفاهم مشترک: درک مشترک نیازها را در بین اعضای تیم ترویج می کند.
همسویی با اهداف تجاری: تضمین می کند که تلاش های توسعه با اهداف تجاری و نیازهای کاربر همسو هستند.
تشخیص زودهنگام مسائل: سناریوهای BDD به عنوان معیارهای پذیرش اولیه عمل میکنند و به تیمها اجازه میدهند مسائل و سوء تفاهمها را در مراحل اولیه توسعه شناسایی کنند.
همکاری بهبود یافته: همکاری بین توسعهدهندگان، آزمایشکنندگان و سهامداران تجاری را تشویق میکند و احساس مشترک مالکیت و مسئولیت را نسبت به کیفیت نرمافزار تقویت میکند.
توسعه رفتار محور (BDD) یک روش قدرتمند برای توسعه نرم افزار است که بر رفتارها و الزامات سیستم از دیدگاه ذینفعان آن تمرکز دارد. با استفاده از سناریوهای زبان ساده و تستهای خودکار، BDD همکاری، درک مشترک و همسویی با اهداف تجاری را ارتقا میدهد و در نهایت منجر به نرمافزار با کیفیت بالاتری میشود که نیازهای کاربران خود را بهتر برآورده میکند.
نتیجه گیری
درک و اجرای استراتژی های تست موثر یک اصل اساسی برای توسعه نرم افزار حرفه ای است. با استفاده از Mocks، Stubs و Spies، توسعهدهندگان میتوانند اجزای جداگانه را جداسازی و آزمایش کنند و از عملکرد صحیح هر قسمت اطمینان حاصل کنند. تست End-to-End این اطمینان را ایجاد می کند که کل سیستم از دیدگاه کاربر به طور هماهنگ کار می کند. معیارهای پوشش کد به شناسایی شکافها در آزمایش کمک میکند و باعث بهبود جامعیت آزمون میشود. بکارگیری توسعه آزمایش محور (TDD) و توسعه مبتنی بر رفتار (BDD) فرهنگ کیفیت، وضوح و همکاری را تقویت می کند و تضمین می کند که نرم افزار نه تنها الزامات فنی را برآورده می کند، بلکه با اهداف تجاری و انتظارات کاربر همسو می شود. به عنوان توسعه دهندگان، اعمال و اصلاح این شیوه ها در طول زمان به ما امکان می دهد راه حل های نرم افزاری قابل اعتماد، پایدار و موفق تری بسازیم.
اطمینان از کیفیت و قابلیت اطمینان کد ضروری است! این اغلب به معنای استفاده از روشها و ابزارهای مختلف تست برای تأیید عملکرد نرمافزار مطابق انتظار است. بهعنوان توسعهدهندگان، بهویژه آنهایی که تازه وارد این حوزه شدهاند، درک مفاهیمی مانند تستهای واحد، ساختگیها، آزمایشهای سرتاسر، توسعه تست محور (TDD) و برخی دیگر از مفاهیمی که در این مقاله بیشتر به آنها خواهیم پرداخت، بسیار مهم است. هر یک از اینها نقش مهمی در اکوسیستم آزمایش ایفا می کند و به تیم ها کمک می کند تا برنامه های کاربردی قوی، قابل نگهداری و قابل اعتماد ایجاد کنند. هدف این است که برخی از تکنیک ها و مفاهیم تست، ارائه توضیحات و مثال های عملی را توضیح دهیم تا بتوان کمی بیشتر در مورد تست نرم افزار به خصوص در اکوسیستم جاوا اسکریپت فهمید.
تست های واحد
تستهای واحد جنبهای اساسی از تست نرمافزاری هستند که بر تأیید عملکرد اجزا یا واحدهای کد، معمولاً توابع یا روشها تمرکز دارند. هدف این تستها اطمینان از این است که هر واحد کد به صورت مجزا و بدون تکیه بر سیستمها یا وابستگیهای خارجی مطابق انتظار عمل میکند.
تست های واحد چیست؟
-
تست گرانول: تستهای واحد کوچکترین بخشهای یک برنامه کاربردی، مانند توابع یا روشها را هدف قرار میدهند.
-
انزوا: کد مورد آزمایش را از سایر بخش های برنامه و وابستگی های خارجی جدا می کنند.
-
خودکار: تستهای واحد معمولاً خودکار هستند و به آنها اجازه میدهند در طول توسعه مکررا اجرا شوند.
چرا از تست های واحد استفاده کنیم؟
-
تشخیص زودهنگام باگ: آنها در مراحل اولیه توسعه اشکالات را پیدا می کنند و رفع آنها را آسان تر و ارزان تر می کند.
-
کیفیت کد: تست های واحد با اطمینان از اینکه هر واحد کد به درستی کار می کند، کیفیت بهتر کد را ارتقا می دهد.
-
Refactoring Safety: آنها یک شبکه ایمنی را هنگام بازفرآوری کد ارائه می دهند و اطمینان می دهند که تغییرات باعث ایجاد اشکالات جدید نمی شوند.
-
مستندات: آزمونهای واحد به عنوان شکلی از مستندسازی عمل میکنند و نشان میدهند که واحدها چگونه باید عمل کنند.
سناریوی دنیای واقعی
سناریویی را در نظر بگیرید که در آن تابعی دارید که فاکتوریل یک عدد را محاسبه می کند. میخواهید مطمئن شوید که این عملکرد برای ورودیهای مختلف، از جمله موارد لبه، به درستی کار میکند.
کد مثال
در اینجا یک پیاده سازی ساده از یک تابع فاکتوریل و تست های واحد مربوط به آن با استفاده از Jest آورده شده است:
// factorial.js
function factorial(n) {
if (n 0) throw new Error('Negative input is not allowed');
if (n === 0) return 1;
return n * factorial(n - 1);
}
module.exports = factorial;
تست های واحد
// factorial.test.js
const factorial = require('./factorial');
describe('Factorial Function', () => {
it('should return 1 for input 0', () => {
expect(factorial(0)).toBe(1);
});
it('should return 1 for input 1', () => {
expect(factorial(1)).toBe(1);
});
it('should return 120 for input 5', () => {
expect(factorial(5)).toBe(120);
});
it('should throw an error for negative input', () => {
expect(() => factorial(-1)).toThrow('Negative input is not allowed');
});
});
توضیح
-
پیاده سازی تابع: تابع فاکتوریل فاکتوریل یک عدد را به صورت بازگشتی با مدیریت خطا برای ورودی های منفی محاسبه می کند.
-
مجموعه تست: با استفاده از Jest، یک مجموعه تست (شرح) و چندین تست (it) برای ورودی های مختلف تعریف می کنیم.
-
موارد تست:
• اولین آزمایش بررسی می کند که آیا تابع 1 را برای ورودی 0 برمی گرداند یا خیر.
• تست دوم بررسی می کند که آیا تابع 1 را برای ورودی 1 برمی گرداند یا خیر.
• تست سوم بررسی می کند که آیا تابع برای ورودی 5 عدد 120 را برمی گرداند یا خیر.
• تست چهارم بررسی می کند که آیا تابع برای ورودی منفی خطا می دهد یا خیر.
مزایای آزمون های واحد
-
سرعت: تست های واحد سریع اجرا می شوند زیرا واحدهای کوچک کد را به صورت مجزا آزمایش می کنند.
-
قابلیت اطمینان: نتایج ثابتی را ارائه می دهند و به حفظ قابلیت اطمینان بالا در پایگاه های کد کمک می کنند.
-
پیشگیری از رگرسیون: با اجرای مکرر تست های واحد، توسعه دهندگان می توانند رگرسیون ها را در اوایل چرخه توسعه مشاهده کنند.
بهترین روش ها برای نوشتن تست های واحد
-
آزمون ها را کوچک و متمرکز نگه دارید: هر آزمون باید یک رفتار یا سناریوی خاص را تأیید کند.
-
از نام های توصیفی استفاده کنید: نام آزمون ها باید به وضوح آنچه را که آزمایش می کنند را توصیف کند.
-
از وابستگی های خارجی اجتناب کنید: وابستگی های خارجی را مسخره یا خرد کنید تا تست ها را ایزوله و سریع نگه دارید.
-
تست ها را به طور مکرر اجرا کنید: تست های واحد را در خط لوله ادغام پیوسته خود ادغام کنید تا آنها را در هر تغییر کد اجرا کنید.
-
کاور Edge Cases: مطمئن شوید که تست ها موارد لبه، از جمله شرایط خطا و مقادیر مرزی را پوشش می دهند.
ادغام تست های واحد در گردش کار توسعه شما می تواند به طور قابل توجهی قابلیت اطمینان و قابلیت نگهداری نرم افزار شما را افزایش دهد زیرا آنها بخش مهمی از یک استراتژی تست جامع هستند.
مسخره می کند
مسخره کردن یک مفهوم اساسی در تست نرم افزار است، به ویژه در هنگام برخورد با وابستگی هایی که تست را دشوار می کند. به بیان ساده، مسخره کردن است اشیایی که رفتار اشیاء واقعی را شبیه سازی می کنند به صورت کنترل شده این به شما این امکان را می دهد که کد خود را به صورت مجزا با جایگزین کردن وابستگی های واقعی با وابستگی های ساختگی آزمایش کنید.
چرا از Mocks استفاده کنیم؟
-
انزوا: کد خود را مستقل از سیستم ها یا سرویس های خارجی (مانند پایگاه داده ها، API ها و غیره) تست کنید.
-
سرعت: از تأخیر تماسهای شبکه یا عملیات پایگاه داده جلوگیری کنید تا آزمایشها سریعتر شود.
-
کنترل کنید: سناریوهای مختلف از جمله موارد لبه و شرایط خطا را شبیه سازی کنید، که ممکن است بازتولید آنها با وابستگی های واقعی دشوار باشد.
-
قابلیت اطمینان: اطمینان حاصل کنید که تست ها به طور مداوم بدون تاثیر محیط خارجی اجرا می شوند.
سناریوی دنیای واقعی
تصور کنید یک UserService دارید که باید کاربران جدیدی را با ذخیره اطلاعات آنها در پایگاه داده ایجاد کند. در طول آزمایش، به دلایل مختلف (سرعت، هزینه، یکپارچگی داده ها) نمی خواهید عملاً عملیات پایگاه داده را انجام دهید. در عوض، شما از یک ماک برای شبیه سازی تعامل پایگاه داده استفاده می کنید. با استفاده از یک mock، می توانید اطمینان حاصل کنید که متد saveUser هنگام اجرای createUser به درستی فراخوانی می شود.
بیایید این سناریو را با استفاده از Node.js با یک راهاندازی آزمایشی شامل Jest برای تمسخر کلاس پایگاه داده و تأیید تعاملات در UserService بررسی کنیم.
کد مثال
UserService.ts
export class UserService {
private db;
constructor(db) {
this.db = db;
}
getUser(id: string) {
return this.db.findUserById(id);
}
createUser(user) {
this.db.saveUser(user);
}
}
پایگاه داده.ts
export class Database {
findUserById(id: string) {
// Simulate database lookup
return { id, name: "John Doe" };
}
saveUser(user) {
// Simulate saving user to the database
}
}
تست با Mock
import { UserService } from './UserService';
import { Database } from './Database';
jest.mock('./Database');
describe('UserService - Mocks', () => {
let userService;
let mockDatabase;
beforeEach(() => {
mockDatabase = new Database();
userService = new UserService(mockDatabase);
});
it('should call saveUser when createUser is called', () => {
const user = { id: '123', name: 'Alice' };
userService.createUser(user);
expect(mockDatabase.saveUser).toHaveBeenCalled();
expect(mockDatabase.saveUser).toHaveBeenCalledWith(user);
});
});
توضیح
-
راه اندازی Mocks: قبل از هر تست، ما یک mock برای کلاس Database با استفاده از Jest برای شبیه سازی رفتار متد saveUser تنظیم می کنیم.
-
تعریف رفتار: ما اطمینان میدهیم که mockDatabase.saveUser با شیء کاربر صحیح هنگام اجرای createUser فراخوانی میشود.
-
**مورد تست: **ما بررسی می کنیم که createUser به درستی saveUser را با جزئیات کاربر ارائه شده فراخوانی می کند.
با استفاده از ماک ها، ما UserService را از پایگاه داده واقعی جدا می کنیم و محیط آزمایش را کنترل می کنیم، و مطمئن می شویم که تست های ما قابل اعتماد و کارآمد هستند. این رویکرد در بسیاری از زبان های برنامه نویسی و چارچوب های آزمایشی رایج است و آن را به یک مفهوم جهانی در توسعه نرم افزار تبدیل می کند.
خرد
Stubs، مانند mock ها، دو تست هستند که برای شبیه سازی رفتار اشیاء واقعی به روشی کنترل شده در طول آزمایش استفاده می شوند. با این حال، برخی از تفاوت های کلیدی بین خرد و مسخره وجود دارد.
خرد چیست؟
خرد هستند پاسخ های از پیش تعریف شده به تماس های خاص در طول آزمون ساخته شده است. بر خلاف ماکها، که میتوانند برای تأیید تعاملات و رفتارها (مانند اطمینان از فراخوانی روشهای خاص) نیز مورد استفاده قرار گیرند، استابها عمدتاً بر ارائه خروجیهای کنترلشده برای فراخوانیهای متد متمرکز هستند.
چرا از Stubs استفاده کنیم؟
-
کنترل کنید: پاسخهای از پیش تعیینشده را به فراخوانیهای متد ارائه دهید، و از نتایج آزمون ثابت اطمینان حاصل کنید.
-
انزوا: کد مورد آزمایش را از وابستگی های خارجی، شبیه به ماک ها جدا کنید.
-
سادگی: تنظیم و استفاده در زمانی که فقط نیاز به کنترل مقادیر برگشتی دارید و تعاملات را تأیید نمی کنید، اغلب ساده تر است.
سناریوی دنیای واقعی
سناریویی را در نظر بگیرید که در آن سرویسی دارید که قیمت کل اقلام موجود در سبد خرید را محاسبه می کند. این سرویس برای دریافت قیمت هر کالا به سرویس دیگری متکی است. در طول آزمایش، شما نمی خواهید به خدمات واقعی واکشی قیمت تکیه کنید، بنابراین از خرد برای شبیه سازی رفتار استفاده می کنید.
کد مثال
پیاده سازی خدمات
// cartService.js
class CartService {
constructor(priceService) {
this.priceService = priceService;
}
async calculateTotal(cart) {
let total = 0;
for (let item of cart) {
const price = await this.priceService.getPrice(item.id);
total += price * item.quantity;
}
return total;
}
}
module.exports = CartService;
تست با Stubs
// cartService.test.js
const chai = require('chai');
const sinon = require('sinon');
const CartService = require('./cartService');
const expect = chai.expect;
describe('CartService', () => {
let priceServiceStub;
let cartService;
beforeEach(() => {
priceServiceStub = {
getPrice: sinon.stub()
};
cartService = new CartService(priceServiceStub);
});
it('should calculate the total price of items in the cart', async () => {
priceServiceStub.getPrice.withArgs(1).resolves(10);
priceServiceStub.getPrice.withArgs(2).resolves(20);
const cart = [
{ id: 1, quantity: 2 },
{ id: 2, quantity: 1 }
];
const total = await cartService.calculateTotal(cart);
expect(total).to.equal(40);
});
it('should handle an empty cart', async () => {
const cart = [];
const total = await cartService.calculateTotal(cart);
expect(total).to.equal(0);
});
});
توضیح
-
راه اندازی خرد: قبل از هر آزمایش، ما priceServiceStub را با استفاده از Sinon ایجاد میکنیم تا روش getPrice را خرد کنیم.
-
رفتار را تعریف کنید: ما رفتار PriceServiceStub را برای ورودی های خاص تعریف می کنیم:
• withArgs(1).resolves(10) getPrice(1) را 10 برمی گرداند.
•withArgs(2).resolves(20) getPrice(2) را 20 برمی گرداند. -
موارد تست:
• در اولین آزمایش، تأیید می کنیم که CalculTotal به درستی قیمت کل اقلام موجود در سبد خرید را محاسبه می کند.
• در تست دوم، بررسی میکنیم که در صورت خالی بودن سبد،calculTotal 0 برمیگردد.
با خردهها، CartService را از خدمات واکشی قیمت واقعی جدا میکنیم و مقادیر بازگشتی کنترلشده را ارائه میکنیم، و از نتایج آزمایشی ثابت و قابل اعتماد اطمینان میدهیم. Stubs زمانی مفید است که شما نیاز به کنترل مقادیر برگشتی متدها بدون تأیید برهمکنش بین اشیا دارید، که آنها را جایگزین سادهتری برای mock در بسیاری از سناریوها میکند.
جاسوس ها
جاسوس ها نوع دیگری از تست های دوگانه هستند که در تست واحد برای مشاهده رفتار توابع استفاده می شوند. برخلاف تمسخر و خرد، جاسوس ها در درجه اول استفاده می شوند نظارت بر نحوه فراخوانی توابع در طول اجرای آزمون آنها میتوانند توابع یا روشهای موجود را بپیچند و به شما این امکان را میدهند که بررسی کنید که آیا و چگونه فراخوانی شدهاند، بدون اینکه لزوماً رفتار آنها را تغییر دهید.
جاسوس ها چیست؟
جاسوس ها برای موارد زیر استفاده می شوند:
-
پیگیری تماسهای تابع: بررسی کنید که آیا یک تابع فراخوانی شده است، چند بار و با چه آرگومان هایی فراخوانی شده است.
-
نظارت بر تعاملات: تعامل بین قسمت های مختلف کد را مشاهده کنید.
-
بررسی عوارض جانبی: اطمینان حاصل کنید که برخی از توابع به عنوان بخشی از اجرای کد فراخوانی شده اند.
چرا از جاسوس استفاده کنیم؟
-
غیر مزاحم: جاسوسها میتوانند روشهای موجود را بدون تغییر رفتارشان بپیچانند و باعث میشوند که آنها کمتر مداخله کنند.
-
تأیید: برای تأیید اینکه برخی روش ها یا توابع به درستی در طول آزمایش ها فراخوانی می شوند عالی است.
-
انعطاف پذیری: می تواند همراه با خرد و ماک برای تست جامع استفاده شود.
سناریوی دنیای واقعی
تصور کنید یک NotificationService دارید که اعلانها را ارسال میکند و این اقدامات را ثبت میکند. شما می خواهید مطمئن شوید که هر بار که اعلان ارسال می شود، به درستی ثبت شده است. به جای جایگزینی عملکرد گزارش، می توانید از یک جاسوس برای نظارت بر تماس های روش گزارش استفاده کنید.
کد مثال
NotificationService.ts
export class NotificationService {
private logger;
constructor(logger) {
this.logger = logger;
}
sendNotification(message: string) {
// Simulate sending a notification
this.logger.log(`Notification sent: ${message}`);
}
}
Logger.ts
export class Logger {
log(message: string) {
console.log(message);
}
}
تست با جاسوس
import { NotificationService } from './NotificationService';
import { Logger } from './Logger';
import { jest } from '@jest/globals';
describe('NotificationService - Spies', () => {
let notificationService;
let logger;
beforeEach(() => {
logger = new Logger();
notificationService = new NotificationService(logger);
});
it('should call log method when sendNotification is called', () => {
const logSpy = jest.spyOn(logger, 'log');
const message = 'Hello, World!';
notificationService.sendNotification(message);
expect(logSpy).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith(`Notification sent: ${message}`);
});
});
توضیح
-
سرویس راه اندازی: قبل از هر آزمایش، ما یک notificationService با استفاده از یک نمونه لاگر ایجاد می کنیم تا از روش log جاسوسی کنیم.
-
فراخوانی روش ها: روش sendNotification را در نمونه notificationService با یک پیام آزمایشی فراخوانی می کنیم.
-
تأیید تماس ها: بررسی می کنیم که هنگام ارسال اعلان، متد log و با آرگومان صحیح فراخوانی شود.
Spies به ما امکان می دهد بدون تغییر رفتار آن، تأیید کنیم که متد log همانطور که انتظار می رود فراخوانی می شود. جاسوس ها به ویژه برای تأیید تعاملات و عوارض جانبی در کد شما مفید هستند و آنها را به ابزاری ارزشمند برای اطمینان از صحت رفتار برنامه شما در طول آزمایش تبدیل می کند.
تست های یکپارچه سازی
تستهای یکپارچهسازی برای تأیید اینکه ماژولهای مختلف یک برنامه نرمافزاری همانطور که انتظار میرود تعامل دارند، مهم هستند. برخلاف تستهای واحد که بر واحدهای تک کد تمرکز میکنند، تستهای یکپارچهسازی همکاری بین اجزای یکپارچه را ارزیابی میکنند و هر مشکلی را که ممکن است از عملکرد ترکیبی آنها ناشی شود، شناسایی میکنند.
تست های یکپارچه سازی چیست؟
-
اجزای ترکیبی: تست های یکپارچه سازی میزان کارکرد قطعات ترکیب شده یک سیستم را ارزیابی می کنند.
-
محیط واقعی: این تست ها اغلب از سناریوهای واقعی تری در مقایسه با تست های واحد، شامل پایگاه های داده، API های خارجی و سایر اجزای سیستم استفاده می کنند.
-
تست میان افزار: میان افزار و اتصالات بین قسمت های مختلف سیستم را تست می کنند.
چرا از تست های یکپارچه سازی استفاده کنیم؟
-
شناسایی مشکلات رابط: آنها به شناسایی مسائل در مرزهایی که اجزای مختلف با هم تعامل دارند کمک می کنند.
-
از هم افزایی اجزا اطمینان حاصل کنید: بررسی کنید که قسمت های مختلف سیستم همانطور که انتظار می رود با هم کار می کنند.
-
قابلیت اطمینان سیستم: قابلیت اطمینان کلی سیستم را با تشخیص خطاهایی که ممکن است در تست های واحد از دست برود، افزایش دهید.
-
سناریوهای پیچیده: سناریوهای پیچیده تر و دنیای واقعی را که شامل چندین بخش از سیستم می شوند، آزمایش کنید.
سناریوی دنیای واقعی
یک برنامه وب با یک API پشتیبان و یک پایگاه داده را در نظر بگیرید. شما می خواهید اطمینان حاصل کنید که یک نقطه پایانی API خاص به درستی داده ها را از پایگاه داده بازیابی می کند و آنها را در قالب مورد انتظار برمی گرداند.
کد مثال
در اینجا یک مثال ساده از یک تست یکپارچه سازی برای یک برنامه Node.js با استفاده از Jest و Supertest آورده شده است:
// app.js
const express = require('express');
const app = express();
const { getUser } = require('./database');
app.get('/user/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).send('User not found');
}
} catch (error) {
res.status(500).send('Server error');
}
});
module.exports = app;
تست های یکپارچه سازی
// app.test.js
const request = require('supertest');
const app = require('./app');
const { getUser } = require('./database');
jest.mock('./database');
describe('GET /user/:id', () => {
it('should return a user for a valid ID', async () => {
const userId = '1';
const user = { id: '1', name: 'John Doe' };
getUser.mockResolvedValue(user);
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(200);
expect(response.body).toEqual(user);
});
it('should return 404 if user is not found', async () => {
const userId = '2';
getUser.mockResolvedValue(null);
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(404);
expect(response.text).toBe('User not found');
});
it('should return 500 on server error', async () => {
const userId = '3';
getUser.mockRejectedValue(new Error('Database error'));
const response = await request(app).get(`/user/${userId}`);
expect(response.status).toBe(500);
expect(response.text).toBe('Server error');
});
});
توضیح
-
راه اندازی برنامه: برنامه Express مسیری را تعریف می کند که کاربر را با شناسه از پایگاه داده دریافت می کند.
-
تمسخر وابستگی ها: تابع getUser از ماژول پایگاه داده برای شبیه سازی سناریوهای مختلف مسخره می شود.
-
مجموعه تست:
• اولین آزمایش بررسی می کند که آیا نقطه پایانی داده های کاربر صحیح را برای یک شناسه معتبر برمی گرداند یا خیر.
• تست دوم بررسی می کند که اگر کاربر پیدا نشود وضعیت 404 برگردانده می شود.
• تست سوم تضمین می کند که در صورت خطای سرور، وضعیت 500 برگردانده می شود.
مزایای تست های یکپارچه سازی
-
پوشش جامع: آنها با اعتبارسنجی تعاملات بین مؤلفه های متعدد، پوشش آزمون جامع تری را ارائه می دهند.
-
شناسایی مسائل پنهان: اشکالاتی را که ممکن است هنگام آزمایش اجزا به صورت مجزا آشکار نشوند، پیدا کنید.
-
افزایش اعتماد به نفس: افزایش اطمینان در عملکرد و قابلیت اطمینان کلی سیستم.
-
سناریوهای دنیای واقعی: سناریوهایی را آزمایش کنید که از نزدیک استفاده در دنیای واقعی برنامه را تقلید می کنند.
بهترین روش ها برای نوشتن تست های یکپارچه سازی
-
محیط های واقع گرایانه: از محیط هایی که شباهت زیادی به تولید دارند برای کشف مسائل خاص محیط استفاده کنید.
-
مدیریت داده ها: برای اطمینان از اجرای آزمایشها با حالتهای شناخته شده و قابل پیشبینی، دادههای آزمایش را تنظیم و از بین ببرید.
-
خدمات خارجی ساختگی: وابستگی ها و سرویس های خارجی را مسخره کنید تا روی یکپارچگی بین اجزای خود تمرکز کنید.
-
تست تعاملات کلیدی: روی آزمایش مسیرهای حیاتی و تعاملات کلیدی بین اجزای سیستم تمرکز کنید.
-
با تست های واحد ترکیب شود: از تست های یکپارچه سازی همراه با تست های واحد برای پوشش کامل استفاده کنید.
تست های پایان به انتها
تستهای End-to-End (E2E) نوعی آزمایش هستند که بر تأیید عملکرد کامل یک برنامه تمرکز میکنند و اطمینان حاصل میکنند که از ابتدا تا انتها طبق برنامه کار میکند. بر خلاف تست های واحد که اجزا یا عملکردهای جداگانه را آزمایش می کنند، تست های E2E تعاملات واقعی کاربر را شبیه سازی کنید و کل سیستم را آزمایش کنید، از جمله قسمت جلو، باطن و پایگاه داده.
تست های End-to-End چیست؟
-
گردش کار کامل را تست کنید: آنها جریان کامل برنامه را از رابط کاربری (UI) به باطن و برگشت آزمایش می کنند.
-
شبیه سازی اقدامات کاربر واقعی: آنها تعاملات کاربر مانند کلیک کردن روی دکمه ها، پر کردن فرم ها و پیمایش در برنامه را شبیه سازی می کنند.
-
اطمینان از یکپارچگی: آنها بررسی می کنند که تمام قسمت های سیستم به درستی با هم کار می کنند.
چرا از تست های End-to-End استفاده کنیم؟
-
پوشش جامع: آنها بالاترین سطح اطمینان را از عملکرد برنامه به طور کلی ارائه می دهند.
-
مسائل ادغام را بگیرید: آنها مشکلاتی را که هنگام تعامل بخش های مختلف سیستم رخ می دهد شناسایی می کنند.
-
کاربر محور: آنها تأیید می کنند که برنامه از دیدگاه کاربر به درستی رفتار می کند.
سناریوی دنیای واقعی
بیایید سناریویی را در نظر بگیریم که در آن شما یک برنامه وب با قابلیت ورود کاربر دارید. یک تست E2E برای این سناریو شامل باز کردن صفحه ورود، وارد کردن نام کاربری و رمز عبور، کلیک کردن روی دکمه ورود به سیستم و تأیید اینکه کاربر با موفقیت وارد شده و به داشبورد هدایت شده است، میشود.
ما یک تست ساده E2E با استفاده از موکا، چای و سوپرتست برای آزمایش یک برنامه کاربردی Node.js ایجاد خواهیم کرد.
کد مثال
پیاده سازی مسیر باطن
// app.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'john_doe' && password === 'password123') {
return res.status(200).send({ message: 'Welcome, John Doe' });
}
return res.status(401).send({ message: 'Invalid credentials' });
});
app.get('/dashboard', (req, res) => {
res.status(200).send({ message: 'This is the dashboard' });
});
module.exports = app;
تست E2E با موکا، چای و سوپرتست
// app.test.js
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('./app');
const expect = chai.expect;
chai.use(chaiHttp);
describe('User Login E2E Test', () => {
it('should log in and redirect to the dashboard', (done) => {
chai.request(app)
.post('/login')
.send({ username: 'john_doe', password: 'password123' })
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.property('message', 'Welcome, John Doe');
chai.request(app)
.get('/dashboard')
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.property('message', 'This is the dashboard');
done();
});
});
});
it('should not log in with invalid credentials', (done) => {
chai.request(app)
.post('/login')
.send({ username: 'john_doe', password: 'wrongpassword' })
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body).to.have.property('message', 'Invalid credentials');
done();
});
});
});
توضیح
-
تست راه اندازی: ما یک مجموعه آزمایشی را با استفاده از موکا و چای توصیف می کنیم (شرح) و موارد آزمایشی (آن را).
-
شبیه سازی اقدامات کاربر:
• ما از chai.request(app) برای شبیه سازی درخواست های HTTP به برنامه استفاده می کنیم.
• .post('/login') یک درخواست POST را با نام کاربری و رمز عبور به نقطه پایانی ورود ارسال می کند.
• .send({ username: 'john_doe', password: 'password123' }) اعتبار ورود به سیستم را ارسال می کند. -
بررسی نتایج:
• وضعیت پاسخ و پیام را بررسی می کنیم تا ورود موفقیت آمیز را تأیید کنیم.
• ما یک درخواست GET را به نقطه پایانی داشبورد ارسال می کنیم و پاسخ را بررسی می کنیم تا پس از ورود به سیستم، دسترسی به داشبورد را تأیید کنیم. -
رسیدگی به خطاها: ما همچنین سناریویی را که در آن اعتبارنامه های ورود نامعتبر است آزمایش می کنیم و پیام خطا و کد وضعیت مناسب را تأیید می کنیم.
مزایای تست E2E
-
اعتبار سنجی تجربه کاربر: تست های E2E تضمین می کند که برنامه تجربه کاربری خوبی را ارائه می دهد.
-
تست جامع: آنها کل پشته برنامه را آزمایش میکنند و مشکلاتی را که ممکن است تستهای واحد یا یکپارچهسازی از دست بدهند، تشخیص میدهند.
-
اتوماسیون: تستهای E2E را میتوان خودکار کرد و به شما این امکان را میدهد که آنها را به عنوان بخشی از خط لوله CI/CD خود اجرا کنید تا مشکلات را قبل از استقرار پیدا کنید.
تست های End-to-End برای تایید عملکرد کامل و تجربه کاربری برنامه شما بسیار مهم هستند. آنها **تضمین می کنند که تمام قسمت های سیستم شما با هم کار می کنند **و سناریوهای کاربر دنیای واقعی به درستی مدیریت می شوند. با استفاده از ابزارهایی مانند Mocha، Chai و Supertest، میتوانید این تستها را خودکار کنید تا اطمینان بالایی نسبت به کیفیت و قابلیت اطمینان برنامه خود داشته باشید.
پوشش کد
پوشش کد معیاری است که در تست نرم افزار برای اندازه گیری میزان اجرای کد منبع یک برنامه هنگام اجرای یک مجموعه آزمایشی خاص استفاده می شود. کمک می کند تعیین کنید چه مقدار از کد شما در حال آزمایش است و می تواند مناطقی از پایگاه کد را شناسایی کند که تحت هیچ آزمایشی قرار نگرفته اند.
مفاهیم کلیدی پوشش کد
-
پوشش بیانیه: درصد دستورات اجرایی را که اجرا شده اند اندازه گیری می کند.
-
پوشش شعبه: درصد شعب (نقاط تصمیم گیری مانند شرایط if-else) را که اجرا شده اند را اندازه گیری می کند.
-
پوشش عملکرد: درصد توابع یا متدهایی که فراخوانی شده اند را اندازه گیری می کند.
-
پوشش خط: درصد خطوط کد اجرا شده را اندازه گیری می کند.
-
پوشش شرایط: درصد عبارات فرعی بولی را در شرایط شرطی که هم درست و هم نادرست ارزیابی شده اند را اندازه گیری می کند.
چرا از پوشش کد استفاده کنیم؟
-
کد تست نشده را شناسایی کنید: به شما کمک می کند قسمت هایی از پایگاه کد خود را پیدا کنید که تحت آزمایش نیستند.
-
بهبود کیفیت تست: اطمینان حاصل می کند که آزمایشات شما کامل است و سناریوهای مختلف را پوشش می دهد.
-
کیفیت کد را حفظ کنید: با تشویق تست های جامع تر، شیوه های نگهداری بهتر کد را ترویج می کند.
-
اشکالات را کاهش دهید: با اطمینان از تست بیشتر کد شما، احتمال ابتلا به اشکالات و خطاها را افزایش می دهد.
نمونه ای از پوشش کد
یک تابع ساده و تست های آن را در نظر بگیرید:
پیاده سازی
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, multiply, subtract };
تست ها
// math.test.js
const chai = require('chai');
const expect = chai.expect;
const { add, multiply, subtract } = require('./math');
describe('Math Functions', () => {
it('should add two numbers', () => {
expect(add(2, 3)).to.equal(5);
});
it('should multiply two numbers', () => {
expect(multiply(2, 3)).to.equal(6);
});
it('should subtract two numbers', () => {
expect(subtract(5, 3)).to.equal(2);
});
});
ایجاد پوشش کد
برای اندازه گیری پوشش کد، می توانیم از ابزاری مانند استانبول (که اکنون NYC نامیده می شود) استفاده کنیم. در اینجا نحوه تنظیم آن آمده است:
- NYC را نصب کنید: ابتدا، NYC را به عنوان وابستگی توسعه نصب کنید.
npm install --save-dev nyc
- NYC را پیکربندی کنید: یک پیکربندی در package.json خود اضافه کنید یا یک فایل .nycrc ایجاد کنید.
// package.json
"nyc": {
"reporter": ["html", "text"],
"exclude": ["test"]
}
- تست ها را با پوشش اجرا کنید: اسکریپت آزمایشی خود را طوری تغییر دهید که شامل NYC باشد.
// package.json
"scripts": {
"test": "nyc mocha"
}
- تست ها را اجرا کنید: تست های خود را با دستور coverage اجرا کنید.
npm test
تفسیر گزارش های پوشش کد
پس از اجرای آزمایشها، NYC یک گزارش پوشش ایجاد میکند. این گزارش معمولاً شامل:
-
خلاصه: خلاصه ای از درصدهای پوشش برای دستورات، شاخه ها، توابع و خطوط.
-
گزارش تفصیلی: گزارش مفصلی که نشان می دهد کدام خطوط کد پوشش داده شده است و کدام نه.
خروجی نمونه (ساده شده):
=============================== Coverage summary ===============================
Statements : 100% ( 12/12 )
Branches : 100% ( 4/4 )
Functions : 100% ( 3/3 )
Lines : 100% ( 12/12 )
================================================================================
پوشش کد یک معیار خوب در تست نرم افزار است که به شما کمک می کند تا مطمئن شوید کد شما به خوبی تست شده و قابل اعتماد است. با استفاده از ابزارهایی مانند NYC، می توانید اندازه گیری و تجسم کنید که چه مقدار از کد شما توسط آزمایش ها پوشش داده شده است، شکاف ها را شناسایی کرده و کیفیت کلی پایگاه کد خود را بهبود بخشید. پوشش کد بالا می تواند به طور قابل توجهی خطر اشکالات را کاهش دهد و قابلیت نگهداری نرم افزار شما را بهبود بخشد.
توسعه آزمایش محور
توسعه تست محور (TDD) یک رویکرد توسعه نرم افزار است که در آن تست ها قبل از کد واقعی نوشته می شوند. این فرآیند ابتدا بر نوشتن یک آزمون مردود، سپس نوشتن حداقل کد مورد نیاز برای قبولی در آن آزمون، و در نهایت تغییر کد برای مطابقت با استانداردهای قابل قبول تأکید دارد. هدف TDD این است که اطمینان حاصل کند که کد قابل اعتماد، قابل نگهداری است و از ابتدا الزامات را برآورده می کند.
مفاهیم کلیدی توسعه آزمایش محور
-
چرخه قرمز-سبز-رفاکتور:
• قرمز: یک آزمایش برای یک عملکرد یا ویژگی جدید بنویسید. در ابتدا، آزمایش ناموفق خواهد بود زیرا این ویژگی هنوز اجرا نشده است.
• سبز: حداقل کد لازم برای قبولی در آزمون را بنویسید.
• Refactor: کد جدید را اصلاح کنید تا ساختار و خوانایی آن بدون تغییر رفتار بهبود یابد. اطمینان حاصل کنید که همه آزمایشها پس از فاکتورسازی مجدد همچنان انجام میشوند. -
تکرارهای کوچک: TDD تغییرات کوچک و تدریجی را تشویق می کند. هر تکرار شامل نوشتن یک تست، قبولی آن و سپس بازآفرینی است.
-
روی الزامات تمرکز کنید: تست های نوشتاری ابتدا توسعه دهندگان را مجبور می کند تا قبل از اجرا، الزامات و طراحی ویژگی را در نظر بگیرند.
چرا از توسعه تست محور استفاده کنیم؟
-
بهبود کیفیت کد: TDD منجر به کدهایی با طراحی بهتر، تمیزتر و قابل نگهداری تر می شود.
-
اشکال زدایی کمتر: اشکالات در مراحل اولیه توسعه یافت می شوند و زمان صرف شده برای اشکال زدایی را کاهش می دهند.
-
درک بهتر نیازها: نوشتن تست ها ابتدا به روشن شدن الزامات و طراحی قبل از اجرا کمک می کند.
-
پوشش تست بالا: از آنجایی که تست ها برای هر ویژگی نوشته می شوند، TDD پوشش کد بالایی را تضمین می کند.
نمونه ای از توسعه تست محور
بیایید یک مثال ساده از پیادهسازی یک تابع را برای بررسی اول بودن عدد با استفاده از TDD در جاوا اسکریپت با Mocha و Chai مرور کنیم.
مرحله 1: نوشتن یک آزمون ناموفق (قرمز)
ابتدا یک تست برای تابع isPrime می نویسیم که هنوز وجود ندارد.
// isPrime.test.js
const chai = require('chai');
const expect = chai.expect;
const { isPrime } = require('./isPrime');
describe('isPrime', () => {
it('should return true for prime number 7', () => {
expect(isPrime(7)).to.be.true;
});
it('should return false for non-prime number 4', () => {
expect(isPrime(4)).to.be.false;
});
it('should return false for number 1', () => {
expect(isPrime(1)).to.be.false;
});
it('should return false for number 0', () => {
expect(isPrime(0)).to.be.false;
});
it('should return false for negative numbers', () => {
expect(isPrime(-3)).to.be.false;
});
});
تست را اجرا کنید، زیرا تابع isPrime هنوز تعریف نشده است.
مرحله 2: نوشتن کد حداقلی برای قبولی در آزمون (سبز)
سپس حداقل کد مورد نیاز برای قبولی در آزمون را می نویسیم.
// isPrime.js
function isPrime(num) {
if (num 1) return false;
for (let i = 2; i num; i++) {
if (num % i === 0) return false;
}
return true;
}
module.exports = { isPrime };
دوباره تست را اجرا کنید. این بار باید بگذرد
مرحله 3: کد را اصلاح کنید
در نهایت، ما کد را بازسازی می کنیم تا کارایی و خوانایی آن را بدون تغییر رفتار آن بهبود ببخشیم.
// isPrime.js
function isPrime(num) {
if (num 1) return false;
if (num 3) return true;
if (num % 2 === 0 || num % 3 === 0) return false;
for (let i = 5; i * i num; i += 6) {
if (num % i === 0 || num % (i + 2) === 0) return false;
}
return true;
}
module.exports = { isPrime };
تست ها را دوباره اجرا کنید تا مطمئن شوید که همه آنها هنوز پس از بازآفرینی موفق هستند.
مزایای TDD
-
اعتماد به کد: اطمینان حاصل می کند که تغییرات کد باگ جدیدی ایجاد نمی کند.
-
مستندات: تست ها به عنوان شکلی از مستندات عمل می کنند و نمونه هایی از نحوه عملکرد کد را ارائه می دهند.
-
بهبودهای طراحی: طراحی و معماری بهتر نرم افزار را تشویق می کند.
-
کاهش زمان رفع اشکال: تشخیص زودهنگام اشکال زمان صرف شده برای اشکال زدایی را به حداقل می رساند.
Test Driven Development (TDD) یک متدولوژی قدرتمند است که به توسعه دهندگان کمک می کند کدی با کیفیت بالا، قابل اعتماد و قابل نگهداری ایجاد کنند. با پیروی از چرخه Red-Green-Refactor، توسعه دهندگان می توانند اطمینان حاصل کنند که کد آنها از ابتدا الزامات را برآورده می کند و پوشش تست بالایی را حفظ می کند. TDD منجر به طراحی بهتر، اشکال زدایی کمتر و اطمینان بیشتر در پایگاه کد می شود.
توسعه رفتار محور
توسعه مبتنی بر رفتار (BDD) یک متدولوژی توسعه نرم افزار است که اصول توسعه آزمایش محور (TDD) را با تمرکز بر روی رفتار سیستم از دیدگاه ذینفعان آن. BDD بر همکاری بین توسعه دهندگان، آزمایش کنندگان و ذینفعان تجاری تاکید می کند تا اطمینان حاصل شود که نرم افزار رفتارها و الزامات مورد نظر را برآورده می کند.
مفاهیم کلیدی توسعه رفتار محور
-
تفاهم مشترک: BDD همکاری و ارتباط بین اعضای تیم را تشویق می کند تا اطمینان حاصل شود که همه درک روشنی از رفتار مطلوب سیستم دارند.
-
داستان ها و سناریوهای کاربر: BDD از داستان ها و سناریوهای کاربر برای توصیف رفتار مورد انتظار سیستم به زبان ساده استفاده می کند که هم برای ذینفعان فنی و هم غیر فنی قابل درک باشد.
-
نحو داده شده-وقتی-پس: سناریوهای BDD معمولاً از یک قالب ساختاریافته به نام Given-When-Then پیروی می کنند که زمینه اولیه (Given)، اقدام در حال انجام (When) و نتیجه مورد انتظار (Then) را توصیف می کند.
-
آزمون های پذیرش خودکارسناریوهای BDD اغلب با استفاده از چارچوبهای آزمایشی خودکار میشوند و به آنها اجازه میدهند هم به عنوان مشخصات اجرایی و هم بهعنوان تست رگرسیون عمل کنند.
چرا از توسعه مبتنی بر رفتار استفاده کنیم؟
-
وضوح و درک: BDD درک مشترک نیازها را در بین اعضای تیم ترویج می کند و ابهامات و سوء تفاهم ها را کاهش می دهد.
-
همسویی با اهداف تجاری: با تمرکز بر رفتارها و داستان های کاربر، BDD تضمین می کند که تلاش های توسعه با اهداف تجاری و نیازهای کاربر همسو هستند.
-
تشخیص زودهنگام مسائل: سناریوهای BDD به عنوان معیارهای پذیرش اولیه عمل میکنند و به تیمها اجازه میدهند مسائل و سوء تفاهمها را در مراحل اولیه توسعه شناسایی کنند.
-
همکاری بهبود یافته: BDD همکاری بین توسعهدهندگان، آزمایشکنندگان و ذینفعان کسبوکار را تشویق میکند و یک احساس مشترک مالکیت و مسئولیت نسبت به کیفیت نرمافزار را تقویت میکند.
نمونه ای از توسعه رفتار محور
بیایید یک مثال ساده از پیاده سازی یک ویژگی برای برداشت پول از ATM با استفاده از BDD با نحو Gherkin و Cucumber.js را در نظر بگیریم.
فایل ویژگی (ATMWithdrawal.feature)
Feature: ATM Withdrawal
As a bank customer
I want to withdraw money from an ATM
So that I can access my funds
Scenario: Withdrawal with sufficient balance
Given my account has a balance of $100
When I withdraw $20 from the ATM
Then the ATM should dispense $20
And my account balance should be $80
Scenario: Withdrawal with insufficient balance
Given my account has a balance of $10
When I withdraw $20 from the ATM
Then the ATM should display an error message
And my account balance should remain $10
تعاریف مرحله (atmWithdrawal.js)
const { Given, When, Then } = require('cucumber');
const { expect } = require('chai');
let accountBalance = 0;
let atmBalance = 100;
Given('my account has a balance of ${int}', function (balance) {
accountBalance = balance;
});
When('I withdraw ${int} from the ATM', function (amount) {
if (amount > accountBalance) {
this.errorMessage = 'Insufficient funds';
return;
}
accountBalance -= amount;
atmBalance -= amount;
this.withdrawnAmount = amount;
});
Then('the ATM should dispense ${int}', function (amount) {
expect(this.withdrawnAmount).to.equal(amount);
});
Then('my account balance should be ${int}', function (balance) {
expect(accountBalance).to.equal(balance);
});
Then('the ATM should display an error message', function () {
expect(this.errorMessage).to.equal('Insufficient funds');
});
اجرای سناریوها
میتوانید سناریوها را با استفاده از Cucumber.js اجرا کنید، که فایل ویژگی را تجزیه میکند، مراحل را با تعاریف آنها مطابقت میدهد و آزمایشها را اجرا میکند.
مزایای BDD
-
تفاهم مشترک: درک مشترک نیازها را در بین اعضای تیم ترویج می کند.
-
همسویی با اهداف تجاری: تضمین می کند که تلاش های توسعه با اهداف تجاری و نیازهای کاربر همسو هستند.
-
تشخیص زودهنگام مسائل: سناریوهای BDD به عنوان معیارهای پذیرش اولیه عمل میکنند و به تیمها اجازه میدهند مسائل و سوء تفاهمها را در مراحل اولیه توسعه شناسایی کنند.
-
همکاری بهبود یافته: همکاری بین توسعهدهندگان، آزمایشکنندگان و سهامداران تجاری را تشویق میکند و احساس مشترک مالکیت و مسئولیت را نسبت به کیفیت نرمافزار تقویت میکند.
توسعه رفتار محور (BDD) یک روش قدرتمند برای توسعه نرم افزار است که بر رفتارها و الزامات سیستم از دیدگاه ذینفعان آن تمرکز دارد. با استفاده از سناریوهای زبان ساده و تستهای خودکار، BDD همکاری، درک مشترک و همسویی با اهداف تجاری را ارتقا میدهد و در نهایت منجر به نرمافزار با کیفیت بالاتری میشود که نیازهای کاربران خود را بهتر برآورده میکند.
نتیجه گیری
درک و اجرای استراتژی های تست موثر یک اصل اساسی برای توسعه نرم افزار حرفه ای است. با استفاده از Mocks، Stubs و Spies، توسعهدهندگان میتوانند اجزای جداگانه را جداسازی و آزمایش کنند و از عملکرد صحیح هر قسمت اطمینان حاصل کنند. تست End-to-End این اطمینان را ایجاد می کند که کل سیستم از دیدگاه کاربر به طور هماهنگ کار می کند. معیارهای پوشش کد به شناسایی شکافها در آزمایش کمک میکند و باعث بهبود جامعیت آزمون میشود. بکارگیری توسعه آزمایش محور (TDD) و توسعه مبتنی بر رفتار (BDD) فرهنگ کیفیت، وضوح و همکاری را تقویت می کند و تضمین می کند که نرم افزار نه تنها الزامات فنی را برآورده می کند، بلکه با اهداف تجاری و انتظارات کاربر همسو می شود. به عنوان توسعه دهندگان، اعمال و اصلاح این شیوه ها در طول زمان به ما امکان می دهد راه حل های نرم افزاری قابل اعتماد، پایدار و موفق تری بسازیم.