5. Хокүрикү (北陸)-гийн хонгил ! Иччораи*!
~Exception тест~
Китано: Дараачийн шинэ тестийг нэмэх үү?
Хиранабэ: Юуг тестлэх үү?
Китано: Pop-н тестийг хиймээр байна. Юуны өмнө огт push хийгээгүй үеийн pop-н тестийг нэмье.
Хиранабэ: Аа, тэр тохиололд EmptyStackException чулуудагдах нь уу гэдгийг шалгахгүй бол болохгүй нь ээ. Энээ хүртэлхээс жаахан өөр маягийн тест болох нь байна.
Китано: Тийм байна. Сонирхолтой тест болохоор хийж үзмээр байсан юм. Иймэрхүү байвал ямар уу?
public void testEmptyPop() {
try {
stack.pop();
fail();
} catch (EmptyStackException exception) {
}
}
* Орч: Иччорай - Хамгийн сайн зүйл гэсэн утгатай үг. Иччора (一張羅) гэдэг үгний Фүкүи муж хавийн нутгийн аялга ажээ. Сонирхуулахад Фүкүи хавийн шинэ үеийн ардын дуу "Дэлгэр цаг" (イッチョライ節) дууны эхний мөрөнд Фүкүи мужын бахархал болсон газруудын нэг болох Хокурикүгийн хонгил гэж гардаг байна. Уг дууг 1937 онд энэ хонгилын нээлтэнд зориулж зохиосон гэдэг.
Хиранабэ: Аанхаан. pop()-н дараа fail() дуудагдвал (exception үүсэхгүй бол) тест амжилтгүй болно. ОК. Тэгээд pop()-дээр
Exception чулуудагдах болохоор [чулуудагдна гэж найдаж буй Exception-г] catch хийж (барьж авч) буй. Ммм.
Китано: Яасан?
Хиранабэ: Catch хийж буй Exception-ий хувьсагчийн нэр exception биш expected (найдаж буй) байвал илуу ойлгомжтой юм биш үү?
Китано: Аа, тийм байна. Ойлголоо.
Китано: За, compile хийгээдэхье. Compile error гарчлаа. pop() байхгүй байна гэж байгаа болохоор pop()-г нэмлээ.
Хиранабэ: За, одоохондоо зөвхөн compile error-г арилгая.
public void pop() {
}
Китано: Ийм болох нь ээ. Тестлээд үзье. Улаан болох ёстой доо.
Хиранабэ: За, улаалаа. pop()-н дараа fail() дуудагдаад байгаа болохоор тэр.
Китано: Тест давах хамгийн бага код бичих гээд үзье.
Китано: pop() дотроос Exception чулуудвал болох нь ээ?
public void pop() {
throw new EmptyStackException();
}
Хиранабэ: За, болж байна. Ногоон болох нь уу шалгая.
Китано: Ногоорчлоо (алгаа ташив).
Хиранабэ: Энэ бол Exception чулуудаж буй хэсгийг тестлэх арга.
Китано: Энэ хэсгийг бас гурвалжлах аргаар шалгая.
Хиранабэ: Нэг удаа push-лэсний хийсний дараа pop-лоход Exception чулуудагдахгүй гэдэг тестийг хийж үзье. Та яаж хийхийг мэдэх үү?
Хиранабэ: Мэднэ. Ийм [тест] болно.
public void testPushAndPop() {
stack.push(1);
stack.pop();
assertEquals(0, stack.size());
}
Хиранабэ: Шалгая. Улайх ёстой.
Китано: Аанхаан. Pop үргэлж Exception чулуудаж байгаа болохоор, Exception үүсч амжилтгүй болно. Run Test! Улаан боллоо.
Хиранабэ: Тэгвэл pop-г ингээд өөрчилчье.
public void pop() {
if (isEmpty())
throw new EmptyStackException();
}
Хиранабэ: isEmpty() true, ө.х size 0 бол EmptyStackException-г чулуудна.
Китано: За. OK, болж байна. size == 0 гэх юм болов уу гэсэн чинь, isEmpty()-ээр болох юм байна (инээмсэглэв).
Хиранабэ: Тестлээд үзье. Ногоон болчлоо (алга ташив).
Китано: Байз? top ч гэсэн EmptyStackException-г чулуудаж байгаа юм байна.
Хиранабэ: Тийм бил үү? Нээрээ тийм. За top-н exception-г тестлэе.
public void testEmptyTop() {
try {
stack.top();
fail();
} catch (EmptyStackException exception) {
}
}
Хиранабэ: Энэ зөв үү?
Китано: Аанхаан. Тестлэе. top()-д exception чулуудах код байхгүй болохоор fail() дуудагдаад, тест амжилтгүй болох ёстой.
Хиранабэ: ОК. Run Test! Улаан аслаа.
Китано: Тэгвэл pop()-той адилханаар засчихъя.
Хиранабэ: Тэгье.
public int top() {
if (isEmpty())
throw new EmptyStackException();
return value;
}
* top(), pop() 2 дээр хийж буй шалгалтыг ерөнхийлөх (тусад нь гаргах) хэрэгтэй гэдгийг анзаарсан ч бай яаран засах хэрэггүй. Юуны өмнө ногоон болгоод, дараад нь refactoring хийх нь зөв хандлага (TDD) юм.
Хиранабэ: Нэг ажиллуулаад шалгачия. Ногоон аслаа.
Китано: Гэхдээ top(), pop() 2-н EmptyStackException чулуудаж буй хэсэг адилхан логиктой болчлоо. Refactoring хийе.
Хиранабэ: Энэ удаагийн refactoring-д ExtractMethod*-г ашиглая.
public int top() {
emptyCheck();
return value;
}
public void pop() {
emptyCheck();
}
private void emptyCheck() {
if (isEmpty())
throw new EmptyStackException();
}
* Орч: Ерөнхий кодыг тусад нь метод болгож гаргахыг хэлж буй бололтой.
Хиранабэ: Үүгээр ногоон болчвол refactoring амжиллтай болжээ гэсэн үг.
6. Хонгилийн үзүүрт?!
~TDD Оргил хэсэг (Last Spurt) ~
Хиранабэ: Ингээд TDD зөв голидролдоо орж эхэллээ шүү! Аль хэдий нь Хокүрикүгийн хонгил ард хоцорчээ. Одоо яах ёстой билээ.
Китано: Pop-лохоор оройн элементийг устгах.
Хиранабэ: Тийм. Хийгээгүй үлдсэн байна. Тест бичээдэхье.
Хиранабэ: Push-лээд pop-лоход size нь 0 болно, энэ тест.
public void testPushAndPop() {
s.push(1);
s.pop();
assertEquals(0, s.size());
}
Китано: Тестээ ажиллуулъя.
Хиранабэ: Улаан болох ёстой доо. Ажиллуулаад үзье. Улаанаар эргэчихлээ.
Китано: pop хийчээд байхад size-н утга өөрчлөгдөхгүй болохоор тэр дээ.
Хиранабэ: За, энэ тестийг амжилттай болгох код бичье.
public void pop() {
emptyCheck();
size--;
}
Китано: pop дуудагдахад size-н утгыг нэгээр хорогдуулна. Ингээд болоо шив дээ.
Хиранабэ: За, ажиллуулъя.
Китано: Ногоон боллоо.
Хиранабэ: Одоо яах билээ?
Китано: За байз, стекийн гол онцлог болох түрүүлж орсоныг сүүлд нь гаргадаг савыг хийгээгүй байгаа гэж бодож байна ... Одоогоор нэг л төвшинтэй стек байгаа болохоор ...
Хиранабэ: За, энэ тестийг бичээд үзье.
public void testPushPushPopTop() {
s.push(1);
s.push(2);
assertEquals(2, stack.size());
s.pop();
assertEquals(1, stack.top());
}
Китано: 2 удаа push, 1 удаа поп хийсний дараа top хийвэл эхний удаагаар push хийсэн утга буцна гэсэн тест болох нь.
Хиранабэ: За, яг энэ байдалтайгаар ямар болох нь уу нэг шалгая. Run Test!
Китано: Улаан болчлоо. Түрүүн хэлсэнээр 1 төвшинтэй болохоор тэр үү?
Хиранабэ: Тэгвэл, олон төвшинтэй болгое. Яаж хийх үү?
Китано: Харин ээ, int төрлийн массив ашиглая.
Хиранабэ: You Have!*
Китано: I Have!*
* You Have! - Чамд даатгалаа шүү!
Онгоцны нисгэгч туслах нисгэгчид жолоо шилжүүлэхэд хэрэглэдэг хэллэг.
* I have! - Ойлголоо!
"You Have!"-н хариу.
private int[] value = new int[10];
public void push(int value) {
this.value[size++] = value;
}
public int top() {
emptyCheck();
return value[size - 1];
}
Китано: size хувьсагчийг массивын индекс болгож ашиглана. Болж байна уу?
Хиранабэ: OK.
Китано: Run test!
Хиранабэ: Ногоон боллоо.
Китано: Ингээд бүх ToDo жагсаалт хоосон боллоо*. Аль хэдий нь Сабаэг өнгөрчиж, удалгүй Фүкүи. 50 мин хэртээ болов уу? энэ TDD-н дадлага.
Хиранабэ: Тийм. Гоё боллоо. Refactoring дахин хийх шаардлагагүй ч боллоо. Гоё цэвэрхэн код (clean code) ч гаргаж авч чадлаа.
Китано: Бүгдийг тэмдэглэж аваад бусдадаа дамжуулмаар юм байна.
Хиранабэ: Тийм шүү.
* Орч: Driver программ бичиж байх хооронд navigator дараагийн алхамуудыг төлөвлөх ба түүнийг driver-т хэлж ToDo жагсаалтанд нэмүүлдэг. Хийж буй зүйлээ дуусангуут дараачийн алхамын талаар зөвлөлдөөд, жагсаалтаас дараачийн ажлыг сонгоно. Гүйцэтгэж дууссан алхамыг жагсаалтаас хасах замаар кодлох нь PP-н бас нэгэн сонгодог арга юм.
//////////// Stack.java ///////////////
import java.util.EmptyStackException;
/**
* @author koji,hiranabe
*/
public class Stack {
private int[] value = new int[10];
private int size;
public boolean isEmpty() {
return size == 0;
}
public int top() {
emptyCheck();
return value[size - 1];
}
public void push(int value) {
this.value[size++] = value;
}
public int size() {
return size;
}
public void pop() {
emptyCheck();
--size;
}
private void emptyCheck() {
if (isEmpty())
throw new EmptyStackException();
}
}
/////////////// StackTest.java /////////////
import java.util.EmptyStackException;
import junit.framework.TestCase;
/**
* @author koji
*/
public class StackTest extends TestCase {
private Stack stack;
protected void setUp() {
stack = new Stack();
}
public void testCreate() {
assertTrue(stack.isEmpty());
}
public void testPushAndTop() {
stack.push(1);
assertFalse(stack.isEmpty());
assertEquals(1, stack.top());
stack.push(2);
assertEquals(2, stack.top());
}
public void testPushAndSize() {
stack.push(1);
assertEquals(1, stack.size());
stack.push(2);
assertEquals(2, stack.size());
}
public void testEmptyPop() {
try {
stack.pop();
fail();
} catch (EmptyStackException expected) {
}
}
public void testPushAndPop() {
stack.push(1);
stack.pop();
assertEquals(0, stack.size());
}
public void testPushPushPopTop() {
stack.push(1);
stack.push(2);
assertEquals(2, stack.size());
stack.pop();
assertEquals(1, stack.top());
}
public void testEmptyTop() {
try {
stack.top();
fail();
} catch (EmptyStackException expected) {
}
}
}
Дүгнэлт
Китано: Сонирхолтой байлаа.
Хиранабэ: Юу нь таалагдав?
Китано: Код нэмэх, өөрчлөх болгонд тест код нэмэгдэж байгаа нь.
Хиранабэ: Аанхаан. Тэгсэнээр бэлэн болсон код тэр чигээрээ тестлэгдсэн
төлөвт орж байгаа юм.
Китано: Бичсэн кодондоо итгэлтэй болох юм билээ. Бас кодлох үед агуулга гэхээсээ илүү интерфейст анхааралаа хандуулах явдал.
Хиранабэ: Яг зөв. Ихэнхи тохиолдол зохиомжилчоод код бичдэг болохоор ихэнхи хүмүүс классын интерфейсийг хэрэгжүүлэгч (дотор=доторхи алгоритм) талаас хараад байдаг. Тест эхлэж бичсэнээр тэр классыг ашиглагчийн байр суурьнаас (гадна талаас нь) харж эхлэдэг зуршилтай болдог. Өөр?
Китано: ?
Хиранабэ: Хамгийн чухал нь классын cпек тестлэх боломжтой спек болж хувирах явдал. Заримдаа тестлэх боломжгүй класс зохиох явдал тохиолддог. Харин XP-д энэ нь байж боломгүй үзэгдэл юм. Заавал тестлэгдэхгүй бол болохгүй. TDD-г хэрэгжүүлбэл тестлэгдэлт (testability-тестлэх боломж) -г үргэлж хангаж чадна.
Китано: Нээрээ тийм байна. Бас "улаан>ногоон>refactoring" нь хөгжүүлэлтийг ритмтэй болгоод, ямар ч байсан зугаатай юм.
Хиранабэ: Good Point. За, нэг дүгнээдэхье.
TDD-н өгөөж:
- Бүх код тестлэгднэ.
- Хэрэгжүүлэлт (алгоритм)-с илүү хэрэглэгч талын интерфейсийг чухалчилсан зохиомжтой болно.
- Тестлэх боломж өндөртэй зохиомж бий болно.
- Хөгжүүлэлт ритмтэй, зовлонгүй болно.
Китано: Хэрэгжүүлж үзээд жаахан залхуутай санагдлаа. Үргэлж fake хийгээд зүгшрүүлээд явах юм уу?
Хиранабэ: Тухайн нөхцөл байдлаас болно. Жишээ нь нэг мөрөөр бичиж болох кодыг fake хийх шаардлага байна уу? гэвэл хийсэн ч болно, хийхгүй байсан ч болно. Шууд эцсийн шийдийг бичих тохиолдол байна. Үүнийг тодорхой кодлолт (Obvious Implementation) гэнэ. Өөртэй итгэлтэй байгаа тохиолдолд тест код бичээд шууд эцсийн шийдийг бичих гэсэн зөвхөн улаан-ногооны хослолоор хийж болно. Харин өөртөө итгэлгүй байгаа бол оромдоод (fake), гурвалжлаад (triangulation), засалт (refactoring) хийгээд зүгшрүүлээд явах хэрэгтэй.
Энэ 2 аргыг солбиж хэрэглэх тухайд, хамтрагчдын өөртөө итгэх итгэл, чадвар, тухайн үеийн нөхцөл байдлаас хамаарна. Нойр дутуу ч юм уу толгой сэргэг биш үед бага багаар оромдож хийх нь ритмтэй болгох сайн тал бий.
Хиранабэ: Дахиад дээрх 3 аргыг нэгтгээд дүгнэе. Ж нь: int add(int a, int b) гэсэн метод бичмээр байлаа гэж саная. Зөв хариу нь return a + b; байг. Тэгвэл 3 арга байна. Эхлээд assertEquals(3, add(1, 2)); гэж тест код бичээд улаан болгоно. Эндээс 3 янзын аргаар цааш нь үргэлжлүүлж болно.
- Fake=>Refactoring Эхлээд return 3; гэж ногоон болгочоод, return a+b;
- Fake=>Triangulation=>Refactoring Эхлээд return 3; гэж кодлоод ногоон болгоно. Дараа нь assertTrue(4, add(3, 1)); гэж тестлээд, улаан болгоно. Хамгийн сүүлд нь return a + b;
- Тодорхой кодлолт () Шууд л return a + b;
Зургаар үзүүлбэл нэг иймэрхүү:
Хиранабэ: Бас нэг зүйл байна. Энд нэг өгөгдлийг танилцуулъя. TDD-г хэрэгжүүлсэн, хэрэгжүүлээгүй үеийн кодны чанарын ялгааны талаар ийм нэгэн өгөгдөл байна.
Хиранабэ: Энэ графикийг хар даа.
Хиранабэ: Энэ бол TDD болон ердийн хөгжүүлэлтийн харьцуулалт. Laurie Williams, Body George 2-н Хойд Каролинагийн их сургууль дээр явуулсан харьцуулсан туршилт. Боулингийн оноо боддог программыг 150 оюутнаар, хосоор программчлах аргаар хийлгэжээ. Программыг хөгжүүлэх хугацаа ердөө 75-хан минут байсан байна.
Китано: Энэ графикийг яаж унших ёстой вэ?
Хиранабэ: Уг туршилтаар 7 хүлээн авах тест* (acceptance test) бэлтгэсэн байсан бөгөөд эдгээр тестээс хэдийг нь амжилттай давсан эсэхийг хэвтээ тэнхлэгээр, түүнд харгалзах хувийг босоо тэнхлэгээр харуулжээ.
Китано: Олон тестийг амжилттай давсан багийн дотроос TDD-р хөгжүүлэлт хийсэн багийн эзлэх хувь илт давуу байна шүү.
Хиранабэ: Тийм. Эндээс TDD-н ашигтай, сайн талыг ойлгосон байх гэж найдаж байна.
Хиранабэ: Ингэсгээд харихгүй бол ...
Китано: За тэгье. Баяртай.
Орчуулсан: Сарту Рэнцэн Батзоригт