Mock object กับการทำ Unit Test

ลองจินตนาการดูว่า ระบบที่เรากำลังสร้างนี้ต้องมีการเชื่อมต่อส่วนอื่นๆ แล้วเขาให้เพียงแค่ Interface มาหนึ่งตัว เราจะเขียนโปรแกรมเพื่อเรียกเขาอย่างไรในเมื่อ Interface มันไม่สามารถทำงานได้ด้วยตัวมันเอง

ตัวอย่างเช่น ผมอยู่ทีมพัฒนาส่วนของ Business Logic แล้วต้องการติดต่อกับทีมที่พัฒนา DAO เพื่อที่จะดึงข้อมูลบางอย่างแล้วทีมนั้นเขาก็โยน Interface มาให้อันนึง บอกว่าให้เรียกผ่านคลาสนี้ แล้วถ้าทีมทำเสร็จมันจะได้ข้อมูลออกมา

เอาหล่ะซิ แล้วจะเขียนโปรแกรมอย่างไรดีเนี่ย ท่ามาตรฐานที่เป็นที่นิยมกันก็จะทำโดยการสร้าง Class ที่ Implement Interface นั้น แล้วจำลองการทำงานด้วยตัวเองไปก่อน พอถึงเวลาจริงค่อยไปใช้คลาสของทีมนั้น ผมมีอีกทางเลือกหนึ่งมาเสนอนั่นก็คือการทำ Mock object

Mock object คืออะไร?

จากเหตุการณ์ที่ยกตัวอย่างมา ถ้าเราเลือกวิธีสร้าง Class เพื่อมาเขียนจำลองการทำงานที่ตัวเองต้องใช้ในช่วงระหว่างรอทีมอื่นพัฒนาอยู่นั้น เราเรียกว่า real object (non-mock object) เพราะฉะนั้น mock object ก็คือตรงข้ามกัน นั่นก็คือ เราไม่ต้องไปสร้างคลาสใหม่ เราสามารถกำหนดพฤติกรรมของ Object ได้ตามใจที่เราต้องการโดยที่เราไม่ต้องไปสร้าง Class ใหม่

ตัวอย่าง: กล่าวคำทักทายโดยดูจากเวลา

ผมขอประยุกต์ใช้ตัวอย่างจากเว็บนี้ ซึ่งเขากำลังเขียนโปรแกรมโดยมี Logic ว่า

if (hourOfDay < 11) {
	word = "Good morning";
}

if (hourOfDay >= 12 && hourOfDay < 17) {
	word = "Good afternoon";
}

else if (hourOfDay >= 17) {
	word = "Good evening";
}

และผมก็จะทำ Unit test ในส่วนของ Logic ที่เขากำลังทำอยู่นี้ โดยผมได้สร้าง Interface ขึ้นมาหนึ่งอันดังนี้


package hello;

public interface ClientService {
	java.util.Date getClientDateTime();
}

จุดประสงค์ที่ผมต้องการคือ อยากสมมุติกรณีที่ผมต้องการดึงค่าวันเวลาของ Client ซึ่งอยู่นอกเหนือการควบคุม โดย Client อาจจะมาจากคนละทวีปทำให้เวลาไม่ตรงกันกับ Server ก็ได้

และคลาสที่เรียกใช้ Interface นี้

package hello;

import java.util.Calendar;
import java.util.Date;

public class WelcomeService {

	private ClientService clientService;

	public void setClientService(ClientService clientService) {
		this.clientService = clientService;
	}

	public String sayHello() {
		Date clientDate = clientService.getClientDateTime();
		Calendar cal = Calendar.getInstance();
		cal.setTime(clientDate);
		int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);

		return sayHello(hourOfDay);
	}

	private String sayHello(int hourOfDay) {
		String word = "";
		if (hourOfDay < 11) {
			word = "Good morning";
		}

		if (hourOfDay >= 12 && hourOfDay < 17) {
			word = "Good afternoon";
		}

		else if (hourOfDay >= 17) {
			word = "Good evening";
		}

		return word;
	}
}

จากตัวอย่างนี้ หากผมต้องการรันโปรแกรม ผมจะต้องสร้างคลาสที่ทำการ Implement ClientService แล้วทำการ new object มาให้คลาสนี้เพื่อที่เวลารันเมธอด sayHello() จะไม่เกิด NullPointerException เมื่อถึงคำสั่ง

Date clientDate = clientService.getClientDateTime();

เพราะว่าเรายังไม่ได้กำหนดค่าให้ตัวแปรนั่นเอง
ให้สร้างคลาสขึ้นมาเพื่อส่ง object ของ Date ดังนี้

package hello;

import java.util.Date;

public class ClientServiceMockup implements ClientService {

	@Override
	public Date getClientDateTime() {
		return new Date();
	}

}

สร้าง jUnit test case ขึ้นมาเพื่อทดสอบ

package test;

import static org.junit.Assert.*;

import org.junit.Test;

public class WelcomeServiceTest {

	@Test
	public void testSayHello() {
		ClientService service = new ClientServiceMockup();
		WelcomeService welcome = new WelcomeService();
		welcome.setClientService(service);
		String hello = welcome.sayHello();
		System.out.println("Hello, " + hello);
	}

}

ผลลัพธ์ที่ได้ดังนี้

เรารันโปรแกรมได้ แต่เรายังไม่ได้ทดสอบอะไรเลย
ซึ่งหากผมต้องการทดสอบให้ครบทั้งสามเคสที่ว่ามา
ผมต้องทำการเปลี่ยนเวลาเครื่องแล้วรันทดสอบก็จะเป็นการสิ้นเปลืองพลังงานเกินไป
เพราะเราต้องการให้เมธอด getClientDateTime() สามารถส่งค่ามา 3 ช่วงเวลาในเมธอดเดียวกัน
ปัญหานี้เราแก้ได้ด้วยวิธี mock object

Mockito

หากเราต้องการที่จะให้ getClientDateTime() ส่งค่าออกมาได้ 3 ช่วงเวลาโดยที่ไม่ต้องไปแก้คลาสหรือปรับเวลาเครื่อง เราสามารถที่จะใช้ mocking framework ช่วยจำลอง object ให้ return ค่า Date ที่เราต้องการได้โดยไม่ต้องแกไขคลาส โดยเราสามารถกำหนดพฤติกรรมของ object นั้นๆ ได้ว่า เมื่อมีการเรียกเมธอดหนึ่งๆ จะให้ return ค่าตามที่เราต้องการได้ และ Mockito นี่หล่ะจะมาเป็นพระเอกให้เรา ซึ่งตัว Mockito เป็น mocking framework ที่นิยมมากในการทำ mock object

ให้ทำการดาวโหลด mockito แล้วนำ jar มาไว้ใน build path ก็สามารถใช้งานได้แล้ว
จากนั้นแก้ไข test case เมื่อซักครู่ให้เป็นดังนี้

package hello;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.util.Calendar;

import org.junit.Test;

public class WelcomeServiceTest {

	@Test
	public void testSayHello() {
		Calendar cal = Calendar.getInstance();
		
		ClientService service = mock(ClientService.class);
		WelcomeService welcome = new WelcomeService();
		welcome.setClientService(service);
		
		cal.set(2012, 9, 14, 8, 0);
		when(service.getClientDateTime()).thenReturn(cal.getTime());
		String helloMorning = welcome.sayHello();
		assertEquals("Good morning", helloMorning);
		
		cal.set(2012, 9, 14, 14, 0);
		when(service.getClientDateTime()).thenReturn(cal.getTime());
		String helloAfternoon = welcome.sayHello();
		assertEquals("Good afternoon", helloAfternoon);
		
		cal.set(2012, 9, 14, 22, 0);
		when(service.getClientDateTime()).thenReturn(cal.getTime());
		String helloEvening = welcome.sayHello();
		assertEquals("Good evening", helloEvening);
	}

}

อธิบายโค้ด

ClientService service = mock(ClientService.class);

เป็นการสร้าง mock object โดยใช้ mockito เป็นคนสร้างให้
จากนั้นผมได้สร้างเคสทดสอบ Good morning โดยสร้าง instance ของ Calendar เพื่อให้สามารถกำหนดเวลาได้

จากนั้นผมได้ทำการกำหนดพฤติกรรมของ object นี้โดยใช้คำสั่ง

when(service.getClientDateTime()).thenReturn(cal.getTime());

แปลได้ตรงตัวเลยว่า เมื่อมีการเรียกเมธอด getClientDateTime() ให้ return ค่าที่ได้จากปฏิทินที่ผมกำหนดเวลาเป็นช่วงเช้า

ซึ่งเมื่อ jUnit ทำการ assertEquals กับค่า “Good morning” ก็จะทำให้ค่าตรงกันแล้ว
และผมก็ทำกับเคสอื่นๆ ตามโค้ดที่ได้เห็นไปก่อนหน้า

และเมื่อผมรัน test case นี้ก็จะสามารถผ่านการทดสอบได้แล้ว

สรุป

ผมสามารถทดสอบโปรแกรมในส่วนของ Interface ได้โดยไม่ต้องรอการ Implement จากทีมอื่นเลย และผมก็ไม่จำเป็นต้องสร้างคลาสที่ทำการ Implement Interface ตัวนั้นแล้วจำลองการทำงานข้างในเอง สังเกตุได้จาก ผมไม่ได้เรียกใช้งานคลาส ClientServiceMockup ใน test case ล่าสุดที่แก้ไขเลย

หลักการ mock object นี้ช่วยให้เราสามารถพัฒนาโปรแกรมได้เร็วขึ้น สามารถกำหนดค่าที่ return มาได้ตามที่ต้องการ ซึ่งเป็นประโยชน์มากในการทำ Unit test ที่ต้องสมมุติเคสแปลกๆ ที่อาจจะเกิดขึ้นได้ยากแต่เราต้องทำการทดสอบเพื่อไม่ให้โปรแกรมเรามีอันเป็นไปเมื่อเคสนั้นเกิดขึ้นจริง

One thought on “Mock object กับการทำ Unit Test

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s