Mock phương thức static trong Unit Test sử dụng PowerMock

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Trong bài viết này, mình sẽ hướng dẫn các bạn Mock các phương thức static trong Unit Test các bạn nhé!

Nếu bạn nào chưa biết về Mock trong Unit Test thì mình có thể nói sơ qua như thế này: Mock là giải pháp giúp chúng ta tạo ra một đối tượng giả để chúng ta có thể chỉ định hành vi của một đối tượng nào đó trong Unit Test. Ví dụ như, bạn có một đối tượng kết nối tới SFTP server nhưng trong Unit Test bạn không thể thiết lập một SFTP server thực được, do đó không thể test hàm kết nối của đối tượng này tới SFTP server được. Trong trường hợp này, bạn có thể tạo một đối tượng Mock để giả lập hành vi của đối tượng kết nối tới SFTP server và chỉ định khi đối tượng khác gọi tới hàm kết nối SFTP server của đối tượng này thì đối tượng này sẽ trả về giá trị gì.

  AI-Powered Future: Data drive product - Trí tuệ nhân tạo vận hành Thế giới
  AspectMock là gì? Tại sao dùng AspectMock với Codeception

Tại sao chúng ta phải sử dụng PowerMock để Mock các phương thức static? Đơn giản là bởi vì chỉ có PowerMock mới hỗ trợ cho chúng ta làm điều này.

OK, bây giờ chúng ta đi vào chủ đề chính của bài viết này nhé các bạn!

Mình sẽ tạo một project Maven để làm ví dụ cho các bạn dễ hiểu. Project của mình như sau:

Mock phương thức static trong Unit Test sử dụng PowerMock

Bây giờ chúng ta sẽ thêm dependency cần thiết của JUnit và PowerMock vào project của chúng ta nhé các bạn! Các bạn hãy mở tập tin pom.xml và thêm những dependency sau:

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

Và class có chứa phương thức static mà chúng ta cần Mock sẽ có nội dung như sau:

package com.huongdanjava.mockstatic;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class SSHCommandLineHelper {

/**
* Execute command: "<strong>ssh <i>server</i> test -e <i>remoteFilePath</i> && echo 1 || echo 0</strong>" to check whether
* a file is existing or not in remote SSH server.
* <p />
* The pre-condition is: we can use SSH to login into other server without username and password.
*
* @param server
* SSH server.
* @param remoteFilePath
* Path of remote file on SSH server.
* @return True if executing success otherwise will false.
*/
public boolean checkFileExistingWithoutUsernamePassword(String server, String remoteFilePath) {
StringBuilder sb = new StringBuilder();
sb.append("ssh ");
sb.append(server);
sb.append(" test -e ");
sb.append(remoteFilePath);
sb.append(" && echo 1 || echo 0");

return executeCommandOnRemote(sb.toString());
}

/**
* Execute the command on remote SSH server.
*
* @param command
* The command.
* @return True if executing success otherwise will false.
*/
private boolean executeCommandOnRemote(String command) {
StringBuffer output = new StringBuffer();

Process p;
try {
p = Runtime.getRuntime().exec(command);
p.waitFor();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));

String line = "";
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
} catch (Exception e) {
e.printStackTrace();
}

return "1".equals(output.toString().trim());
}
}

Mock phương thức static trong Unit Test sử dụng PowerMock

Mục đích của class này là sử dụng Java để thực thi một command line kiểm tra sự tồn tại của một tập tin ở một máy tính khác sử dụng SSH mà không cần phải sử dụng user và password để đăng nhập (để chạy một chương trình sử dụng class này, chúng ta phải cài đặt việc đăng nhập tới máy khác không cần user với password trước).

Các bạn có thể thấy, trong class này, mình đã sử dụng đối tượng Runtime với phương thức static là getRuntime() để thực thi một command line trong Java. Trong Unit Test, làm sao mà chúng ta có thể simulate một máy tính khác để test phương thức static checkFileExistingWithoutUsernamePassword trong class SSHCommandLineHelper phải không các bạn? Giải pháp của chúng ta chỉ có thể là dùng Mock mà thôi.

Đầu tiên, các bạn hãy tạo một class SSHCommandLineHelperTest nằm trong thư mục test với package là com.huongdanjava.mockstatic như sau:

Mock phương thức static trong Unit Test sử dụng PowerMock

Trong class này, đầu tiên các bạn hãy khai báo annotation @PrepareForTest và @RunWith trước như sau:

package com.huongdanjava.mockstatic;

import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({SSHCommandLineHelper.class})
public class SSHCommandLineHelperTest {
}

Trong phần @Before của Unit Test, chúng ta sẽ khai báo sử dụng PowerMock để Mock phương thức static trong class Runtime như sau:

@Before
public void init() {
PowerMockito.mockStatic(Runtime.class);
}

Bây giờ, chúng ta sẽ viết test cho trường hợp khi chúng ta chạy chương trình để kiểm tra tập tin tồn tại trên một máy SSH khác thì phương thức checkFileExistingWithoutUsernamePassword trả về giá trị TRUE nhé các bạn!

Nội dung của phương thức testCheckFileExistingSuccessWithoutUsernamePassword như sau:

@Test
public void testCheckFileExistingSuccessWithoutUsernamePassword() throws IOException {
// Khai báo Mock cho đối tượng Runtime 
Runtime mockedRuntime = PowerMockito.mock(Runtime.class);
PowerMockito.when(Runtime.getRuntime()).thenReturn(mockedRuntime);

// Khai báo Mock cho đối tượng Process
Process mockedProcess = PowerMockito.mock(Process.class);

// Vì đây là trường hợp success nên đối tượng Process sẽ trả về giá trị "1" khi execute command line 
InputStream is = new ByteArrayInputStream("1".getBytes());
PowerMockito.when(mockedProcess.getInputStream()).thenReturn(is);
PowerMockito.when(mockedRuntime.exec(anyString())).thenReturn(mockedProcess);

// Khởi tạo đối tượng SSHCommandLineHelper
SSHCommandLineHelper helper = new SSHCommandLineHelper();

// Assert khi gọi phương thức sẽ trả về TRUE
Assert.assertTrue(helper.checkFileExistingWithoutUsernamePassword("abc", "abc"));
}

Kết quả:

Mock phương thức static trong Unit Test sử dụng PowerMock

Trường hợp chúng ta test cho tập tin không tồn tại thì chỉ cần chỉnh sửa một xíu như sau:

@Test
public void testCheckFileExistingSuccessWithoutUsernamePassword() throws IOException {
// Khai báo Mock cho đối tượng Runtime 
Runtime mockedRuntime = PowerMockito.mock(Runtime.class);
PowerMockito.when(Runtime.getRuntime()).thenReturn(mockedRuntime);

// Khai báo Mock cho đối tượng Process
Process mockedProcess = PowerMockito.mock(Process.class);

// Vì đây là trường hợp không success nên đối tượng Process sẽ trả về giá trị "0" khi execute command line 
InputStream is = new ByteArrayInputStream("0".getBytes());
PowerMockito.when(mockedProcess.getInputStream()).thenReturn(is);
PowerMockito.when(mockedRuntime.exec(anyString())).thenReturn(mockedProcess);

// Khởi tạo đối tượng SSHCommandLineHelper
SSHCommandLineHelper helper = new SSHCommandLineHelper();

// Assert khi gọi phương thức sẽ trả về FALSE
Assert.assertFalse(helper.checkFileExistingWithoutUsernamePassword("abc", "abc"));
}

Kết quả:

Mock phương thức static trong Unit Test sử dụng PowerMock

Toàn bộ code của class SSHCommandLineHelperTest như sau:

package com.huongdanjava.mockstatic;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import static org.mockito.Matchers.anyString;

@RunWith(PowerMockRunner.class)
@PrepareForTest({SSHCommandLineHelper.class})
public class SSHCommandLineHelperTest {

@Before
public void init() {
PowerMockito.mockStatic(Runtime.class);
}

@Test
public void testCheckFileExistingSuccessWithoutUsernamePassword() throws IOException {
Runtime mockedRuntime = PowerMockito.mock(Runtime.class);
PowerMockito.when(Runtime.getRuntime()).thenReturn(mockedRuntime);

Process mockedProcess = PowerMockito.mock(Process.class);
InputStream is = new ByteArrayInputStream("1".getBytes());
PowerMockito.when(mockedProcess.getInputStream()).thenReturn(is);
PowerMockito.when(mockedRuntime.exec(anyString())).thenReturn(mockedProcess);

SSHCommandLineHelper helper = new SSHCommandLineHelper();

Assert.assertTrue(helper.checkFileExistingWithoutUsernamePassword("abc", "abc"));
}

@Test
public void testCheckFileNotExistingWithoutUsernamePassword() throws IOException {
Runtime mockedRuntime = PowerMockito.mock(Runtime.class);
PowerMockito.when(Runtime.getRuntime()).thenReturn(mockedRuntime);

Process mockedProcess = PowerMockito.mock(Process.class);
InputStream is = new ByteArrayInputStream("0".getBytes());
PowerMockito.when(mockedProcess.getInputStream()).thenReturn(is);
PowerMockito.when(mockedRuntime.exec(anyString())).thenReturn(mockedProcess);

SSHCommandLineHelper helper = new SSHCommandLineHelper();

Assert.assertFalse(helper.checkFileExistingWithoutUsernamePassword("abc", "abc"));
}
}