在开发组里,Unit Testing的方法已经深入人心,Case的数量也越来越多。可因为GUI层面代码的特殊性,目前大多数的测试都针对非GUI层面的Code。这使得占总代码量40%的GUI层面很少被单元测试覆盖。
本文通过一个简单的例子,结合SWTBot,Junit和Truth,实现了GUI层面的单元测试。
SWTBot的安装
SWTBot可以通过Update Site(http://download.eclipse.org/technology/swtbot/releases/latest/)安装。
在开发项目中也要加入SWTBot的依赖,
一个简单的Dialog
让我们来实现一个简单的对话框,用来做两个数的加法。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
package swtbot; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.layout.GridData; public class AddDialog extends Dialog { private Text text1; private Text text2; private Label resultLabel; /** * Create the dialog. * @param parentShell */ public AddDialog(Shell parentShell) { super(parentShell); } /** * Create contents of the dialog. * @param parent */ @Override protected Control createDialogArea(Composite parent) { Composite container = (Composite) super.createDialogArea(parent); container.setLayout(new GridLayout(5, false)); Label lblAddCalculation = new Label(container, SWT.NONE); lblAddCalculation.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 5, 1)); lblAddCalculation.setText("Add Calculation"); text1 = new Text(container, SWT.BORDER); GridData gd_add1Text = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); gd_add1Text.widthHint = 50; text1.setLayoutData(gd_add1Text); text1.setData("id", "text1"); text1.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { updateResult(); } }); Label label = new Label(container, SWT.NONE); label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); label.setText("+"); text2 = new Text(container, SWT.BORDER); GridData gd_add2Test = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); gd_add2Test.widthHint = 50; text2.setLayoutData(gd_add2Test); text2.setData("id", "text2"); text2.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { updateResult(); } }); Label label_1 = new Label(container, SWT.NONE); label_1.setText("="); resultLabel = new Label(container, SWT.NONE); resultLabel.setData("id", "result"); resultLabel.setText("No Answer"); return container; } protected void updateResult() { if (text1.getText().isEmpty() || text2.getText().isEmpty()) { return; } int result = Integer.parseInt(text1.getText()) + Integer.parseInt(text2.getText()); resultLabel.setText(Integer.toString(result)); } /** * Create contents of the button bar. * @param parent */ @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); } /** * Return the initial size of the dialog. */ @Override protected Point getInitialSize() { return new Point(266, 135); } } |
这里特别使用了setData方法对几个关键的控件设置了“id”,这是为了能在SWTBot中更方便准确的定位待测试的控件。
SWTBot+JUnit+Truth
待测的对话框已经准备好了,下面来写一个简单的单元测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package swtbot; import java.util.Random; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swtbot.swt.finder.SWTBot; import org.eclipse.swtbot.swt.finder.widgets.SWTBotLabel; import org.eclipse.swtbot.swt.finder.widgets.SWTBotText; import org.junit.Test; import com.google.common.truth.Truth; public class AddDialogTest { @Test public void testAdd() { Display display = Display.getDefault(); Shell shell = new Shell(display); shell.setSize(300, 300); AddDialog dialog = new AddDialog(shell); dialog.setBlockOnOpen(false); dialog.open(); SWTBot bot = new SWTBot(dialog.getShell()); SWTBotText text1 = bot.textWithId("id", "text1"); SWTBotText text2 = bot.textWithId("id", "text2"); SWTBotLabel resultLabel = bot.labelWithId("id", "result"); Random random = new Random(System.currentTimeMillis()); for (int i = 0; i < 1000; ++i) { int num1 = random.nextInt(1000); text1.setText(Integer.toString(num1)); int num2 = random.nextInt(2000); text2.setText(Integer.toString(num2)); Truth.assertThat(resultLabel.getText()).isEqualTo(Integer.toString(num1 + num2)); } bot.button("OK").click(); } } |
几点要注意的地方,
- 测试Dialog时,要使用setBlockOnOpen(false),否则open()方法会把后续测试代码阻塞掉。
- SWTBot搜索控件的办法有几种,以Text控件为例,介绍几个常用的。
- text()等价于text(0), 就是找第一个Text控件
- text(n), 按顺序找第n个控件
- textWithLabelInGroup(label, inGroup)等价于textWithLabelInGroup(label, inGroup,0),就是找在某个Group里的第0个Label为”label”的Text控件
- textWithLabelInGroup(label, inGroup,n),就是找在某个Group里的第n个Label为”label”的Text控件
- textInGroup(text, inGroup)等价于textInGroup(text, inGroup, 0), 找在某个Group里第0个text为“text”的Text控件
- textInGroup(text, inGroup, n), 找在某个Group里第n个text为“text”的Text控件
- textWithId(key, data), 找出key=data的Text控件。这就是和前面setData()相对应的一个API。用ID找还有一个更简单的API–textWithId(value),这个API没有输入key,原因是SWTBot有个Preference给DEFAULT_KEY–org.eclipse.swtbot.search.defaultKey,默认的值为
“org.eclipse.swtbot.widget.key”。
- 对不同的控件,API会略有不同,不过大同小异。SWTBot现在除了支持基本的SWT控件外,还支持了Nebula Grid,NatTable,GEF等复杂的控件。
- 写Assertion的部分和普通的Unit Test并没有什么不同。
- 因为这种Unit Test的写法和普通的JUnit并没有什么不同,所以Headless Build就可以用一般maven test。当然,如果是开发RCP应用,一定要使用Eclipse Tycho插件。
- 还有一点不同的事,SWTBot的Case一定要在X环境下。如果Build Server上并没有开X环境,需要要安装Xvfb。
总结
通过一个小例子,本文讨论了如何使用SWTBot对小型的SWT开发单元,如对话框、Composite等,进行单元测试(SWTBot也可以测试完整的大型应用)。
现在可以慢慢完善GUI层面的单元测试了,下一步也许可以和Cucumber之类的Framework结合起来。😁