تست خودکار در وظیفه های بعدی استفاده خواهد شد، و همچنین به طور گسترده در پروژه های واقعی استفاده می شود.
چرا به تست نیاز داریم؟
وقتی یک تابعی را می نویسیم، معمولاً می توانیم تصور کنیم که چه کاری باید انجام دهد: کدام پارامترها چه نتایجی را ارائه می دهند.
در طول توسعه، میتوانیم تابعی را اجرا کرده و خروجی آن را با چیزی که انتظار داریم تابع به ما بدهد بررسی کنیم. به عنوان مثال، ما می توانیم این کار را در کنسول انجام دهیم.
اگر چیزی اشتباه باشد – کد را تصحیح می کنیم، دوباره از اول اجرا می کنیم، نتیجه را بررسی می کنیم – و به همین ترتیب تا زمانی که کد ما کار کند، این کار ها را انجام می دهیم.
اما چنین “re-runs”(اجرای مجدد) به صورت دستی ناقص می باشد.
هنگام تست یک کد با اجرای مجدد(re-run) به صورت دستی، به راحتی می توانیم چیزی را از قلم بیاندازیم.
به عنوان مثال، ما یک تابع f
ایجاد می کنیم. کدی مینویسیم و تست می کنیم: f(1)
کار می کند، اما f(2)
کار نمی کند. ما کد را اصلاح می کنیم و اکنون f(2)
کار می کند. آیا الان تست ما کامل به نظر می رسد؟ اما فراموش کردیم f(1)
را دوباره تست کنیم، که ممکن است به ارور برخورد کنیم.
این خیلی معمول(عادی) است. وقتی چیزی را توسعه میدهیم، کیس های احتمالی زیادی را در ذهن خود نگه میداریم، اما به سختی می توان انتظار داشت که یک برنامه نویس پس از هر تغییر، همه آنها را به صورت دستی بررسی کند. بنابراین اصلاح یک چیز و خراب کردن یک چیز دیگر آسان می شود.
تست خودکار به این معنی است که تست ها علاوه بر کد، به طور جداگانه نوشته می شوند. آنها تابع های ما را به روش های مختلف اجرا می کنند و نتایج به دست آمده را با آنچه انتظار می رود مقایسه می کنند.
توسعه رفتار محور (behavior driven development) (BDD)
بیایید با تکنیکی به نام Behavior Driven Development یا به طور خلاصه (BDD) شروع کنیم.
این BDD سه قسمت دارد: تست ها، مستندات(داکیومنت ها) و مثال ها.
برای درک بهتر BDD، یک مورد عملی از توسعه را بررسی خواهیم کرد.
توسعه ی “pow”: توضیح:
فرض کنید میخواهیم یک تابع pow(x, n)
بسازیم که x
را به توان یک عدد صحیح n
برساند. ما فرض می کنیم که n≥0
.
این تکلیف فقط یک مثال است: اپراتور **
در جاوا اسکریپت وجود دارد که می تواند این کار را انجام دهد، اما در اینجا ما روی جریان توسعه تمرکز می کنیم که می تواند برای کارهای پیچیده تر نیز اعمال شود.
قبل از ایجاد کد pow
، میتوانیم تصور کنیم که تابع باید چه کاری انجام دهد و چگونه آن را توصیف کنیم.
چنین توصیفی یک specification(مشخصات) یا به طور خلاصه، یک spec نامیده میشود و حاوی توضیحاتی در مورد کیس(مورد) های همراه با تست هایی برای آنها است، مانند این:
describe("pow", function() {
it("به توان n ام افزایش می یابد", function() {
assert.equal(pow(2, 3), 8);
});
});
یک spec دارای سه بلوک اصلی است که می توانید در بالا مشاهده کنید:
describe("موضوع", function() { ... })
-
چه عملکردی را توضیح می دهیم؟ در این کیس، ما تابع
pow
را توصیف می کنیم. برای گروه بندی “کارگران(workers)” – بلوک هایit
استفاده می شود. it("توضیحات کیس مورد نظر", function() { ... })
-
در عنوان
it
ما به روشی قابل خواندن برای انسان کیس مورد نظر را توصیف می کنیم، و آرگومان دوم تابعی است که آن را تست می کند. assert.equal(value1, value2)
-
کد داخل بلوک
it
، در صورتی که پیاده سازی آن صحیح باشد، باید بدون خطا(ارور) اجرا شود.توابع
*.assert
برای بررسی اینکه آیاpow
همانطور که انتظار می رود کار می کند یا نه استفاده می شود. در اینجا ما از یکی از آنها استفاده می کنیم –assert.equal
، آرگومان ها را با هم مقایسه می کند و در صورتی که برابر نباشند، خطا می دهد. در اینجا بررسی میکند که نتیجهpow(2, 3)
برابر8
باشد. انواع دیگری از مقایسه و بررسی وجود دارد که بعداً اضافه خواهیم کرد.
در ابنجا specification را می توان اجرا کرد و تست مشخص شده در بلوک it
را اجرا می کند. بعداً خواهیم دید.
جریان توسعه(The development flow)
جریان توسعه معمولاً به این صورت است:
- یک spec اولیه با تست هایی برای بنیادی(اساسی) ترین عملکرد نوشته شده است.
- یک پیاده سازی اولیه ایجاد می شود.
- برای بررسی اینکه آیا کار می کند یا نه، فریم ورک تست [Mocha] (https://mochajs.org/) (جزئیات بیشتر به زودی) را اجرا می کنیم که spec را اجرا می کند. تا زمانی که عملکرد کامل نباشد، خطاها نمایش داده می شوند. ما اصلاحات را انجام می دهیم تا زمانی که همه چیز درست کار بکند
- اکنون ما یک پیاده سازی اولیه با تست داریم.
- کیس های بیشتری را به spec اضافه می کنیم که احتمالاً هنوز توسط پیاده سازی ها پشتیبانی نشده اند. تست ها به مشکل بر میخورند.
- به شماره 3 برگردید و پیاده سازی ها را آپدیت کنید تا وقتی که تست ها خطایی ندهند.
- مراحل 3-6 را تکرار کنید تا عملکرد ها آماده شود.
بنابراین، توسعه تکرار شونده می باشد. ما spec را مینویسیم، آن را پیادهسازی میکنیم، مطمئن میشویم که تستها قبول شدند، سپس تستهای بیشتری مینویسیم، مطمئن میشویم که کار میکنند و همینطور ادامه میدهیم. در نهایت ما یک پیاده سازی موفق و تست هایی برای آن داریم.
بیایید این جریان توسعه را در مثال عملی خود ببینیم.
مرحله اول در حال حاضر کامل شده است: ما یک spec اولیه برای pow
داریم. اکنون، قبل از پیاده سازی، بیایید از چند کتابخانه جاوا اسکریپت برای اجرای تست ها استفاده کنیم تا ببینیم که آنها کار می کنند (همه تست ها رد شدند).
مشخصات(spec) در عمل
در دوره آموزشی ما، از کتابخانه های جاوا اسکریپت زیر برای تست(آزمایش) استفاده خواهیم کرد:
- Mocha – فریم ورک اصلی: توابع تستی رایج از جمله
spec
وit
و تابع اصلی که تست ها را اجرا میکند را ارائه میکند. - Chai – کتابخانه ای با توابع فراوان که این اجازه را می دهد تا از بسیاری از توابع مختلف استفاده کنیم، در حال حاضر فقط به
assert.equal
نیاز داریم. - Sinon – کتابخانه ای برای جاسوسی از توابع، شبیه سازی توابع(built-in) یا همان توابع داخلی و دیگر موارد، بعداً به آن نیاز خواهیم داشت.
این کتابخانه ها هم برای تست داخل مرورگر و هم برای تست سمت سرور مناسب هستند. در اینجا ما نوع مرورگر را در نظر خواهیم گرفت.
صفحه کامل HTML با این فریم ورک ها و pow
spec:
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the script with tests (describe, it...) -->
<script src="test.js"></script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
<!-- run tests! -->
<script>
mocha.run();
</script>
</body>
</html>
صفحه را می توان به پنج بخش تقسیم کرد:
- قسمت
<head>
– برای اضافه کردن کتاب خانه های خارجی و استایل های برای تست. - قسمت
<script>
با توابعی برای تست, که در مثال ما --با کدpow
. - قسمت تست ها – در مثال ما, اسکریپ خارجی
test.js
که تابعdescribe("pow", ...)
را دارد(مانند مثال بالایی). - المان html مقابل:
<div id="mocha">
برای نمایش نتیجه, Mocha از این تگ استفاده میکند. - با کامند(دستور)
mocha.run()
تست ما شروع میشود.
نتیجه:
در حال حاضر، تست ناموفق است و یک خطا وجود دارد. و این خطا منطقی است زیرا: کدی داخل تابع pow
ننوشته ایم و خالی می باشد، بنابراین pow(2, 3)
به جای 8
undefined
را برمی گرداند.
برای آینده، بیایید توجه داشته باشیم که تست ککنده های سطح بالای بیشتری مانند karma و دیگران وجود دارند که اجرای خودکار بسیاری از تستهای مختلف را راحت کرده اند.
پیاده سازی اولیه(Initial implementation)
بیایید یک پیادهسازی ساده از pow
برای گذراندن تستها ایجاد کنیم:
function pow() {
return 8; // :) we cheat!
}
بسیار عالی! الان کار میکند.
بهبود مشخصات(spec)
کاری که ما الان انجام دادیم قطعا یک تقلب است. این تابع کار نمی کند: تلاش برای محاسبه pow(3, 4)
نتیجه نادرستی می دهد، اما تست ها با موفقیت انجام می شوند.
…اما این وضعیت کاملاً عادی است. در عمل اتفاق می افتد و تست ها قبول می شوند، اما عملکرد تابع کاملا اشتباه می باشد. spec(مشخصات) ما ناقص است و ما باید کیس های بیشتری را به آن اضافه کنیم.
بیایید یک تست دیگر اضافه کنیم تا pow(3, 4) = 81
را بررسی کند.
ما می توانیم یکی از دو روش را برای سازماندهی تست در اینجا انتخاب کنیم:
-
نوع اول – یک
assert
دیگر به همانit
اضافه کنید:describe("pow", function() { it("به توان n ام افزایش می یابد", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); });
-
نوع دوم – دو تا تست درست کنیم:
describe("pow", function() { it("دو به توان 3 میشود 8", function() { assert.equal(pow(2, 3), 8); }); it("سه به توان 4 میشود 81", function() { assert.equal(pow(3, 4), 81); }); });
تفاوت اصلی این است که وقتی assert
باعث ایجاد یک خطا میشود، بلوک it
بلافاصله پایان مییابد. بنابراین، در نوع اول، اگر assert
اول ناموفق باشد، هرگز نتیجه assert
دوم را نخواهیم دید.
جدا کردن تستها از هم برای دریافت اطلاعات بیشتر در مورد آنچه که اتفاق میافتد مفید می باشد، بنابراین نوع دوم بهتر است.
و علاوه بر آن، یک قانون دیگری وجود دارد که رعایت کردن آن بهتر است.
یک تست تنها یک چیز را بررسی می کند.
اگر به تست نگاه کنیم و دو حالت چک کردن(بررسی) مستقل از هم در آن ببینیم، بهتر است آن را به دو دسته ساده تر تقسیم کنیم.
پس بیایید با نوع دوم ادامه دهیم.
و نتیجه:
همانطور که میتوانستیم انتظار داشته باشیم، آزمایش دوم رد شد. مطمئناً، تابع ما همیشه 8
را برمی گرداند، در حالی که assert
انتظار 81
را دارد.
بهبود پیاده سازی(Improving the implementation)
بیایید یک چیز واقعی تری بنویسیم تا تست ها قبول شوند:
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
برای اطمینان از اینکه تابع به خوبی کار می کند یا نه، بیایید آن را برای مقادیر بیشتری تست کنیم. به جای نوشتن بلوکهای it
به صورت دستی، میتوانیم آنها را در assert
تولید کنیم:
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} به توان سه میشود ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
نتیجه به صورت زیر می باشد:
توصیف تودرتو (Nested describe)
ما حتی تست های بیشتری را اضافه می کنیم. اما قبل از آن، اجازه دهید توجه داشته باشیم که تابع کمکی makeTest
و for
باید با هم در یک گروه باشند. ما در تستهای دیگر به makeTest
نیاز نداریم، بلکه فقط در for
مورد نیاز است: وظیفه مشترک آنها این است که بررسی کنند که pow
چگونه به توان داده شده افزایش می یابد.
گروهبندی با assert
تودرتو انجام میشود:
describe("pow", function() {
describe("x به توان سه", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
// ... را می توان اضافه کرد it و هم descibe تستهای بیشتری در اینجا نوشته میشوند، هم
});
describe
تودرتو، “زیرگروه(subgroup)” جدیدی از تستها را تعریف میکند. در خروجی نیز می توانیم تورفتگی عنوان تست (title) را ببینیم:
در آینده میتوانیم it
و describe
بیشتری در سطح های بالا تری با توابع کمکی خود اضافه کنیم، آنها makeTest
را نخواهند دید.
before/after
و beforeEach/afterEach
ما میتوانیم توابع before/after
را بنویسیم که قبل/بعد از اجرای تست ها اجرا میشوند و همچنین توابع beforeEach/afterEach
که قبل/بعد از هر it
اجرا میشوند.
برای مثال:
describe("test", function() {
before(() => alert("تست شروع میشود – قبل از همه تست ها"));
after(() => alert("تست پایان می یابد – بعد از همه تست ها"));
beforeEach(() => alert("قبل از تست - به تست وارد میشود"));
afterEach(() => alert("بعد از تست - از تست خارج میشود"));
it('test 1', () => alert(1));
it('test 2', () => alert(2));
});
دنباله اجرا خواهد شد و:
تست شروع میشود – قبل از همه تست ها (before)
قبل از تست - به تست وارد میشود (beforeEach)
1
بعد از تست - از تست خارج میشود (afterEach)
قبل از تست - به تست وارد میشود (beforeEach)
2
بعد از تست - از تست خارج میشود (afterEach)
تست پایان می یابد – بعد از همه تست ها (after)
معمولاً beforeEach/afterEach
و before/after
برای انجام مقداردهی(initialize) اولیه، صفر کردن شمارنده ها یا انجام کار های دیگری بین تست ها (یا گروه های تست) استفاده می شود.
گسترش spec
عملکرد اصلی pow
کامل است. اولین تکرار توسعه انجام شده است. وقت جشن گرفتن و نوشیدن شامپاین تمام شد – بیایید ادامه دهیم و آن را بهبود ببخشیم.
همانطور که گفته شد، تابع pow(x, n)
برای کار با مقادیر صحیح مثبت n
است.
برای نمایش یک خطای ریاضی، توابع جاوا اسکریپت معمولاً NaN
را برمیگردانند. بیایید همین کار را برای مقادیر نامعتبر n
انجام دهیم.
بیایید ابتدا رفتار را به spec(!) اضافه کنیم:
describe("pow", function() {
// ...
it("برای n های منفی عبارت NaN", function() {
assert.isNaN(pow(2, -1));
});
it("برای مقادیر غیر عددی عبارت NaN", function() {
assert.isNaN(pow(2, 1.5));
});
});
و نتیجه با تست جدید به صورت زیر می باشد:
تست های جدید اضافه شده با شکست مواجه می شوند، زیرا پیاده سازی ما از آنها پشتیبانی نمی کند. BDD اینگونه انجام می شود: ابتدا تست های شکست خورده را می نویسیم و سپس برای آنها پیاده سازی می کنیم.
لطفاً به assert.isNaN
توجه کنید: NaN
بودن را بررسی میکند.
assert دیگری نیز در chai وجود دارد، به عنوان مثال:
assert.equal(value1, value2)
– برابری را بررسی می کندvalue1 == value2
.assert.strictEqual(value1, value2)
– برابری دقیق را بررسی می کندvalue1 === value2
.assert.notEqual
,assert.notStrictEqual
– بررسی معکوس موارد بالا.assert.isTrue(value)
– این را بررسی می کندvalue === true
assert.isFalse(value)
– این را بررسی می کندvalue === false
- … لیست کامل را می توانید در مستندات مشاهده کنید.
بنابراین باید چند خط به pow
اضافه کنیم:
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
الان کار میکند و تمام تست ها نیز قبول شده است:
خلاصه
در BDD ابتدا spec(مشخصات) و سپس پیاده سازی انجام می شود. در پایان نیز ما هم spec و هم کد ها را داریم.
مشخصات(spec) را می توان به سه روش استفاده کرد:
- به صورت Tests – تست ها تضمین می کنند که کد به درستی کار می کند.
- به صورت Docs – عناوین
describe
وit
نشان می دهد که تابع چه کاری انجام می دهد. - به صورت Examples – تست ها در واقع نمونه های کاری هستند که نشان می دهند چگونه می توان از یک تابع استفاده کرد.
با این spec ها, میتوانیم با خیال راحت عملکرد تابع را بهبود بخشیم، تغییر دهیم، حتی از ابتدا بازنویسی کنیم و مطمئن شویم که همچنان درست کار میکند.
این به ویژه در پروژه های بزرگ زمانی که یک تابع در بسیاری از مکان ها استفاده می شود مهم است. وقتی چنین تابعی را تغییر میدهیم، راهی برای بررسی دستی وجود ندارد که آیا هر مکانی که از آن استفاده میکند هنوز درست کار میکند یا خیر.
بدون تست, مردم دو راه دارند:
- برای انجام تغییر که مهم نیست که چه باشد, کاربران ما با اشکال مواجه می شوند، زیرا ما احتمالاً چیزی را به صورت دستی بررسی نمی کنیم.
- یا، اگر مجازات به وجود آمدن ارور یا خطا سخت باشد، همان طور که تست ها وجود ندارند، مردم از تغییر چنین تابع هایی می ترسند، در نتیجه کد ما قدیمی می شود(زیرا میترسند که کد را توسعه دهند و اروری رخ بدهد) و هیج کس مایل نیست که کد را توسعه دهد و این برای توسعه خوب نیست.
تست خودکار برای جلوگیری از این مشکلات کمک می کند!
اگر پروژه با تست ها پوشش داده شود، چنین مشکلی به وجود نمی آید, زیرا پس از هر تغییری، میتوانیم تستهایی را اجرا کنیم و در عرض چند ثانیه تعداد زیادی بررسی را مشاهده کنیم.
علاوه بر این، یک کد خوب تست شده معماری بهتری دارد.
به طور طبیعی, اصلاح و بهبود کد های تست شده به صورت خودکار آسان تر است. اما دلیل دیگری نیز وجود دارد.
برای نوشتن تستها، کد باید به گونهای سازماندهی شود که هر تابع دارای یک وظیفه واضح، ورودی و خروجی کاملاً تعریف شده باشد. این یعنی یک معماری خوب از ابتدا.
در زندگی واقعی, گاهی اوقات آنقدرها هم آسان نیست. گاهی اوقات نوشتن یک مشخصات(spec) قبل از کد واقعی دشوار است، زیرا هنوز مشخص نیست که چگونه باید رفتار کند. اما به طور کلی تست, توسعه را سریعتر و پایدارتر می کند.
بعداً در این دوره آموزشی، با بسیاری از تمرین ها همراه با تست های فیکس شده(baked-in) روبرو خواهید شد. بنابراین نمونه های کاربردی بیشتری خواهید دید.
نوشتن تست ها به دانش خوب جاوا اسکریپت نیاز دارد. اما ما تازه شروع به یادگیری آن کرده ایم. بنابراین، برای حل کردن همه چیز، از هم اکنون نیازی به نوشتن تست ندارید، اما باید بتوانید آنها را بخوانید، حتی اگر کمی پیچیده تر از این فصل باشند.