โดดเดี่ยว unit ด้วย Mockito

posted on 23 Sep 2008 23:05 by wonam  in softdev

ปัญหาที่ใหญ่ที่สุดอย่างหนึ่งของการทำ unit testing ก็คือการที่ unit ที่เราต้องการทดสอบนั้นทำงานร่วมกับ unit อื่น ๆ ในระบบ ทั้งนี้เนื่องด้วยเหตุผล เช่น

  • เวลาผลลัพธ์ผิดพลาด เราอาจไม่ทราบว่า unit ใดกันแน่ที่ผิด, หรือ
  • เกิดการต้องรอกันในการทดสอบ นั่นคือ เราไม่สามารถทดสอบ unit นี้ได้ จนกว่า unit อื่น ๆ จะมีให้ใช้

เนื่องด้วยปัญหาดังกล่าวก็เลยเกิดเทคนิคในการสร้าง "ตัวปลอม" ขึ้นหลายแบบ เพื่อทำให้ทำ unit testing บน unit ที่ขึ้นกับ unit อื่นได้

เราจะทดลองใช้วิธีที่เรียกว่า mocking ครับ

mock คืออะไร? mock ก็คือ object ปลอมที่เราสร้างขึ้นเพื่อใช้แทน object จริง ๆ โดยในการสร้างนี้เราสามารถที่จะ "โปรแกรม" การโต้ตอบของวัตถุนี้ได้ด้วย (แนวคิดที่ใกล้เคียงกัน (แต่ไม่เหมือน) ก็คือการใช้ stub ลองอ่านบทความเรื่อง Mocks Aren't Stubs ของมาร์ติน ฟาวเลอร์ ดูนะครับ)

ปกติใน Java เวลาพูดถึง mock framework คนก็จะพูดถึง EasyMock อย่างไรก็ตามเราจะใช้ framework อีกตัวหนึ่ง ชื่อ Mockito ซึ่งนำแนวคิดของ EasyMock มาพัฒนาต่อแล้วทำให้การใช้สะดวกขึ้นอีก

ถ้าจะทดลองต่อไป ต้องติดตั้ง Mockito ให้เรียบร้อยก่อน โดยดาวน์โหลด Mockito มา (ดาวน์โหลด mockito-all-1.x.jar) แล้วติดตั้งลงไปใน build path ของ project

สำหรับตัวอย่าง ผมคิดไม่ค่อยออก เลยสมมติตัวอย่างที่ตอนเขียนหลาย ๆ จุดไม่ค่อยจะสมเหตุสมผลเท่าใดนัก เอาเป็นว่าเรามีคลาส MailProcessor ที่ต้องรับเมล์มา จากนั้นไปถามตัวตรวจสอบ spam ว่าเป็น spam หรือเปล่า ถ้าไม่เป็นก็โยนเข้ากล่องเมล์ปกติ ถ้าเป็น spam ก็โยนเข้า junk

ผมสมมติว่าเรามีไอเดียคร่าว ๆ แค่นี้ และยังไม่ได้ออกแบบคลาสอะไรเลย ซึ่งปกติมักจะไม่จริง เนื่องจากเรามักมีคลาสหรืออินเตอร์เฟสอื่น ๆ เขียนเอาไว้อยู่แล้ว อย่างไรก็ตามในตัวอย่างนี้เราจะสร้างคลาสอื่น ๆ ไประหว่างที่เขียน test นะครับ

เริ่มต้นเราก็สร้างคลาส MailProcessor เปล่า ๆ ขึ้นมา แล้วก็สร้าง MailProcessorTest มา แล้วก็เขียน test case เลย

กรณีแรกที่จะทดสอบก็คือกรณีที่ได้รับ junk mail นะครับ เราก็ไปสร้างเมท็อด testJunkMail ในคลาส MailProcessorTest ถัดจากนั้นถ้าเราจะทดสอบ unit ได้ ก็ต้องสร้าง MailProcessor มาก่อน

public class MailProcessorTest {
	@Test
	public void testJunkMail() {
		// + สร้าง MailProcessor 
		// + ควรมี mailbox สองอัน แล้วก็มีตัวตรวจ spam
		MailProcessor mp = 
			new MailProcessor(normal, junk, detector);
	}
}

ทีนี้ เราก็เห็นว่าเรามี MailBox สองอัน แล้วก็ควรมีคลาส SpamDetector ด้วย แทนที่จะสร้างคลาสเต็ม ๆ ผมก็ไปสร้าง interface เอาไว้แทน ดังด้านล่าง

public interface MailBox {
}

public interface SpamDetector {
}

แล้วกลับไปประกาศ normal, junk, และ detector ให้เรียบร้อย อย่างไรก็ตามเดี๋ยวเราจะทำปลอมมันเลย ด้วย Mockito ดังนั้นก่อนเขียนเพิ่ม เรา import ตัว Mockito มาเลยแล้วกันครับ โดยสั่ง

import static org.mockito.Mockito.*;

จากนั้นก็ไปประกาศโดยกำหนดค่าเริ่มต้นให้เป็นผลที่ได้จากการเรียกเมท็อด mock

	@Test
	public void testJunkMail() {
		// + สร้างวัตถุปลอม โดยเรียกเมท็อด mock ด้วยคลาสของวัตถุเหล่านั้น
		MailBox normal = mock(MailBox.class);
		MailBox junk = mock(MailBox.class);
		SpamDetector detector = mock(SpamDetector.class);

		MailProcessor mp = 
			new MailProcessor(normal, junk, detector);
	}

พอได้สองอันนี้เราก็ไปประกาศ constructor ของ MailProcessor เอาไว้ก่อนครับ (ผมใช้ Eclipse ครับ กด QuickFix มันทำพวกนี้ให้หมดเลย สุดยอดจริง ๆ)

public class MailProcessor {

	// + เพิ่ม constructor เปล่า ๆ ไว้
	public MailProcessor(MailBox normal, MailBox junk, 
		SpamDetector detector) {
		// TODO Auto-generated constructor stub
	}
}

ทีนี้ก็กลับไปเขียน test ของเราต่อ ทีนี้ เราก็ต้องจัดหาเมล์มาส่งให้กับ processor ก็ลองเขียนคร่าว ๆ ลงไปก่อนครับ ว่าควรจะเรียกประมาณนี้

	public void testJunkMail() {
		// ... ละเอาไว้
		MailProcessor mp = 
			new MailProcessor(normal, junk, detector);

		// + เรียกให้ประมวลผล mail		
		mp.process(mail);
	}

ทีนี้ก็เข้าแบบเดิมครับ mail ควรจะเป็นคลาส Mail ก็ไปสร้าง interface เตรียมไว้ แล้วก็มาประกาศตัวแปรพร้อมทั้งกำหนดค่าเริ่มต้นด้วยเมท็อด mock ครับ

เพิ่มอินเตอร์เฟส

public interface Mail {
}

ประกาศ mail พร้อมทั้งทำปลอมมัน

	public void testJunkMail() {
		// ... ละเอาไว้
		MailProcessor mp = 
			new MailProcessor(normal, junk, detector);

		// + สร้าง mail ปลอม โดยเรียกเมท็อด mock		
		Mail mail = mock(Mail.class);

		mp.process(mail);
	}

แล้วก็เพิ่มเมท็อด process ใน MailProcessor

public class MailProcessor {
	// ... ละไว้ ...

	// + ประกาศเมท็อดว่าง ๆ ไว้
	public void process(Mail mail) {
		// TODO Auto-generated method stub		
	}
}

เอาละครับ ทีนี้ก็ถึงเวลาพิจารณาต่อว่าเจ้า MailProcessor.process จะทำอะไรกับวัตถุต่าง ๆ ที่รายล้อม

อย่างแรกก็ต้องส่ง mail ไปตรวจกับ detector ว่าเป็น spam หรือเปล่า เราจะทดสอบกรณีที่ข้อมูลป้อนเข้าเป็น junkmail ดังนั้น detector จะต้องตอบว่าใช่ spam

เราไปเพิ่มเมท็อดให้กับ SpamDetector สำหรับเรียกถามว่าเป็น spam หรือเปล่า ดังด้านล่าง

public interface SpamDetector {
	// + เพิ่มเมท็อด
	boolean isSpam(Mail mail);
}

ทีนี้ก็ถึงเวลา Mockito ออกโรงแล้วครับ...

เราจะไปกำหนดก่อนเรียก mp.process ว่า ถ้า detector โดนเรียก isSpam โดยส่ง mail มาให้ detector จะตอบว่า true (คือเป็น spam) เราเขียนโดยใช้เมท็อด stub ดังด้านล่างครับ

	public void testJunkMail() {
		// ... ละเอาไว้
		stub(detector.isSpam(mail)).toReturn(true);

		mp.process(mail);
	}

ทีนี้ เวลา mp.process(mail) ทำงานแล้วไปเรียก detector.isSpam ก็จะได้ค่าตามที่เราต้องการ

เสร็จขั้นตอนการเตรียมการและการเรียกใช้ unit แล้ว ก็ถึงเวลาตรวจสอบครับ

เราจะตรวจว่ามีการส่ง object mail ไปให้กับ junk mailbox เราก็เขียนโดยเรียกเมท็อด verify ของ Mockito แบบนี้ครับ

	public void testJunkMail() {
		// ... ละเอาไว้
		mp.process(mail);
		
		// + เพิ่มการตรวจสอบ
		verify(junk).add(mail);
	}

ข้างบนเราตรวจสอบการทำงานว่ามีการเรียกเมท็อด add ด้วยอาร์กิวเมนท์ mail หรือไม่ สังเกตว่าเมท็อด add ยังไม่มีอยู่ในอินเตอร์เฟส MailBox เราก็ไปเพิ่มเสีย

public interface MailBox {
	// + เพิ่มเมท็อด
	void add(Mail mail);
}

กลับไปที่ส่วนทดสอบเราต่อ สังเกตว่าเราตรวจว่ามีการเพิ่ม mail เข้าไปที่ junk หรือเปล่า แต่เราไม่ได้ตรวจว่าไม่ได้มีการเพิ่มใน normal ดังนั้นไปเพิ่มการตรวจสอบว่าจะต้องไม่มีการเรียก add ด้วย Mail ใด ๆ ในส่วนทดสอบของเรา

	public void testJunkMail() {
		// ... ละเอาไว้
		verify(junk).add(mail);

		// + ตรวจว่าไม่มีการ add ไปที่ normal
		verify(normal,never())
			.add((Mail) anyObject());
	}

ทีนี้ก็ทดลองเรียกให้ junit ทำงานได้ครับ จะพบว่ามี error เกิดขึ้น โดยระบุว่าเราไม่ได้เรียก add(mail) ที่ junk ครับ

ถึงเวลาหยุดเขียน test ไว้ก่อน ไปเขียนโปรแกรมคลาส MailProcessor กัน ด้านล่างแสดงโปรแกรมนะครับ

public class MailProcessor {

	private MailBox normalBox;
	private MailBox junkBox;
	private SpamDetector detector;

	public MailProcessor(MailBox normal, MailBox junk, 
			SpamDetector detector) {
		this.normalBox = normal;
		this.junkBox = junk;
		this.detector = detector;
	}

	public void process(Mail mail) {
		if(!detector.isSpam(mail))
			normalBox.add(mail);
		else
			junkBox.add(mail);
	}
}

ทดลองรัน junit อีกที ก็พบว่าทำงานผ่านแล้วนะครับ

สรุปขั้นตอนที่เราทำไปคือ

  • เขียน test case ถ้าพบว่าต้องใช้วัตถุของคลาสอะไรก็สร้าง interface ไว้ แล้วก็สร้าง mock จาก interface นั้น
  • สำหรับ mock ที่มีการโต้ตอบกับ unit เราก็ไปโปรแกรมการทำงานเอาไว้ (ไป stub มัน) ว่าถ้าเจอเรียกแบบนี้ให้ทำอะไร
  • พอเตรียมการโต้ตอบ (ที่จำเป็น) เรียบร้อย ก็เรียกใช้ unit
  • จากนั้นก็ตรวจสอบว่า mock ที่เราสนใจนั้นถูกเรียกใช้อย่างที่ควรถูกเรียกหรือไม่

สังเกตว่าเราสามารถเขียน/ทดสอบคลาส MailProcessor ที่ทำงานร่วมกับคลาสอื่น ๆ ได้ โดยไม่ได้เขียนหรือใช้คลาส (จริง ๆ) เหล่านั้นเลย

เมท็อด testJunkMail เต็ม ๆ แสดงด้านล่างครับ

	@Test
	public void testJunkMail() {
		MailBox normal = mock(MailBox.class);
		MailBox junk = mock(MailBox.class);
		SpamDetector detector = mock(SpamDetector.class);
		
		MailProcessor mp = 
			new MailProcessor(normal, junk, detector);
		
		Mail mail = mock(Mail.class);

		stub(detector.isSpam(mail)).toReturn(true);
		
		mp.process(mail);
		
		verify(junk).add(mail);
		verify(normal,never())
			.add((Mail) anyObject());
	}

Comment

smilebig smileopen-mounthed smileconfused smilesad smileangry smiletonguequestionembarrassedsurprised smilewinkdouble winkcry ???????????????   ??????????????????
smilebig smileopen-mounthed smileconfused smilesad smileangry smiletonguequestionembarrassedsurprised smilewinkdouble winkcry ???????????????

Tweet

รบกวนถามอาจารย์ว่า code ส่วน

verify(normal,never()).add((Mail) anyObject());

1.verify นี้จะมี return type แบบเดียวกับส่ิงที่เราใ่ส่เข้าไปเลยเหรอครับ พอดีสักเกตว่ามี method add ด้วย

2. never , anyObject คืออะไร แล้วใช้ตอนไหนครับ

#1 By xinnix on 2008-09-24 00:58

@xinnix

1. ใช่ครับ
เท่าที่ผมเข้าใจคือ Mockito มันน่าจะคืนตัว mock ของเราออกมาล่ะครับ

2. never กับ anyObject เอาไว้ใช้ประกอบในการสั่ง verify ครับ ตรง never นี่บอกว่าให้ verify ว่าเมท็อดที่เราใช้จะไม่ถูกเรียก ในกรณีนี้คือเมท็อด add จะไม่ถูกเรียกด้วย anyObject ครับ

เวลาใช้มันจะสั่งแปลก ๆ หน่อยครับ เพื่อให้เวลาดู/อ่าน แล้วเหมือนเขียนให้คนอ่านอ่ะครับ

ไว้เดี๋ยวหาตัวอย่างใหม่มาเขียนเพิ่มครับ
ขอบคุณครับ

#2 By wonam on 2008-09-24 01:37

mockito นี่เจ๋งดีครับ ดูโค้ดสวยกว่า easyMock เยอะเลย

จากสภาพทำให้เราเข้าใกล้โอกาสจะเป็น mockist แบบเต็มตัวได้ง่ายขึ้นจริงๆ

#3 By deans4j (124.120.142.33) on 2008-09-24 05:44