用 C 语言对 Gtk+ 应用进行功能测试
GTK 测试

> 
> 这个简单教程教你如何测试你应用的功能。
> 
> 
> 


![Functional testing Gtk+ applications in C](/data/attachment/album/201710/12/132540ni4mi77xmr2m27x2.png "Functional testing Gtk+ applications in C ")


自动化测试用来保证你程序的质量以及让它以预想的运行。单元测试只是检测你算法的某一部分，而并不注重各组件间的适应性。这就是为什么会有功能测试，它有时也称为集成测试。


功能测试简单地与你的用户界面进行交互，无论它是网站还是桌面应用。为了展示功能测试如何工作，我们以测试一个 Gtk+ 应用为例。为了简单起见，这个教程里，我们使用 Gtk+ 2.0 教程的示例。


### 基础设置


对于每一个功能测试，你通常需要定义一些全局变量，比如 “用户交互时延” 或者 “失败的超时时间”（也就是说，如果在指定的时间内一个事件没有发生，程序就要中断）。



```
#define TTT_FUNCTIONAL_TEST_UTIL_IDLE_CONDITION(f) ((TttFunctionalTestUtilIdleCondition)(f))
#define TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME (125000)
#define TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME_LONG (500000)
typedef gboolean (*TttFunctionalTestUtilIdleCondition)(gpointer data);
struct timespec ttt_functional_test_util_default_timeout = {
  20,
  0,
};

```

现在我们可以实现我们自己的超时函数。这里，为了能够得到期望的延迟，我们采用 `usleep` 函数。



```
void
ttt_functional_test_util_reaction_time()
{
  usleep(TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME);
}

void
ttt_functional_test_util_reaction_time_long()
{
  usleep(TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME_LONG);
}

```

直到获得控制状态，超时函数才会推迟执行。这对于一个异步执行的动作很有帮助，这也是为什么采用这么长的时延。



```
void
ttt_functional_test_util_idle_condition_and_timeout(
     TttFunctionalTestUtilIdleCondition idle_condition,
     struct timespec *timeout,
     pointer data)
{
  struct timespec start_time, current_time;

  clock_gettime(CLOCK_MONOTONIC,
                &start_time);

  while(TTT_FUNCTIONAL_TEST_UTIL_IDLE_CONDITION(idle_condition)(data)){
    ttt_functional_test_util_reaction_time();

    clock_gettime(CLOCK_MONOTONIC,
                  &current_time);

    if(start_time.tv_sec + timeout->tv_sec < current_time.tv_sec){
      break;
    }
  }

  ttt_functional_test_util_reaction_time();
}

```

### 与图形化用户界面交互


为了模拟用户交互的操作， [Gdk 库](https://developer.gnome.org/gdk3/stable/) 为我们提供了一些需要的函数。要完成我们的工作，我们只需要如下 3 个函数：


* `gdk_display_warp_pointer()`
* `gdk_test_simulate_button()`
* `gdk_test_simulate_key()`


举个例子，为了测试按钮点击，我们可以这么做：



```
gboolean
ttt_functional_test_util_button_click(GtkButton *button)
{
  GtkWidget *widget;

  GdkWindow *window;

  gint x, y;
  gint origin_x, origin_y;

  if(button == NULL ||
     !GTK_IS_BUTTON(button)){
    return(FALSE);
  }

  widget = button;

  if(!GTK_WIDGET_REALIZED(widget)){
    ttt_functional_test_util_reaction_time_long();
  }

  /* retrieve window and pointer position */
  gdk_threads_enter();

  window = gtk_widget_get_window(widget);

  x = widget->allocation.x + widget->allocation.width / 2.0;
  y = widget->allocation.y + widget->allocation.height / 2.0;

  gdk_window_get_origin(window, &origin_x, &origin_y);

  gdk_display_warp_pointer(gtk_widget_get_display(widget),
                           gtk_widget_get_screen(widget),
                           origin_x + x, origin_y + y);

  gdk_threads_leave();

  /* click the button */
  ttt_functional_test_util_reaction_time();

  gdk_test_simulate_button(window,
                           x,
                           y,
                           1,
                           GDK_BUTTON1_MASK,
                           GDK_BUTTON_PRESS);

  ttt_functional_test_util_reaction_time();

  gdk_test_simulate_button(window,
                           x,
                           y,
                           1,
                           GDK_BUTTON1_MASK,
                           GDK_BUTTON_RELEASE);

  ttt_functional_test_util_reaction_time();

  ttt_functional_test_util_reaction_time_long();

  return(TRUE);
}

```

我们想要保证按钮处于激活状态，因此我们提供一个空闲条件函数：



```
gboolean
ttt_functional_test_util_idle_test_toggle_active(
     GtkToggleButton **toggle_button)
{
  gboolean do_idle;

  do_idle = TRUE;

  gdk_threads_enter();

  if(*toggle_button != NULL &&
     GTK_IS_TOGGLE_BUTTON(*toggle_button) &&
     gtk_toggle_button_get_active(*toggle_button)){
    do_idle = FALSE;
  }

  gdk_threads_leave();

  return(do_idle);
}

```

### 测试场景


因为这个 Tictactoe 程序非常简单，我们只需要确保点击了一个 [**GtkToggleButton**](https://developer.gnome.org/gtk3/stable/GtkToggleButton.html) 按钮即可。一旦该按钮肯定进入了激活状态，功能测试就可以执行。为了点击按钮，我们使用上面提到的很方便的 `util` 函数。


如图所示，我们假设，填满第一行，玩家 A 就赢，因为玩家 B 没有注意，只填充了第二行。



```
GtkWindow *window;
Tictactoe *ttt;

void*
ttt_functional_test_gtk_main(void *)
{
  gtk_main();

  pthread_exit(NULL);
}

void
ttt_functional_test_dumb_player_b()
{
  GtkButton *buttons[3][3];

  guint i;

  /* to avoid race-conditions copy the buttons */
  gdk_threads_enter();

  memcpy(buttons, ttt->buttons, 9 * sizeof(GtkButton *));

  gdk_threads_leave();

  /* TEST 1 - the dumb player B */
  for(i = 0; i < 3; i++){
    /* assert player A clicks the button successfully */
    if(!ttt_functional_test_util_button_click(buttons[0][i])){
      exit(-1);
    }

    functional_test_util_idle_condition_and_timeout(
         ttt_functional_test_util_idle_test_toggle_active,
         ttt_functional_test_util_default_timeout,
         &buttons[0][i]);

    /* assert player B clicks the button successfully */
    if(!ttt_functional_test_util_button_click(buttons[1][i])){
      exit(-1);
    }

    functional_test_util_idle_condition_and_timeout(
         ttt_functional_test_util_idle_test_toggle_active,
         ttt_functional_test_util_default_timeout,
         &buttons[1][i]);
  }
}

int
main(int argc, char **argv)
{
  pthread_t thread;

  gtk_init(&argc, &argv);

  /* start the tictactoe application */
  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  ttt = tictactoe_new();
  gtk_container_add(window, ttt);

  gtk_widget_show_all(window);

  /* start the Gtk+ dispatcher */
  pthread_create(&thread, NULL,
                 ttt_functional_test_gtk_main, NULL);

  /* launch test routines */
  ttt_functional_test_dumb_player_b();

  /* terminate the application */
  gdk_threads_enter();

  gtk_main_quit();

  gdk_threads_leave();

  return(0);
}

```

（题图：opensource.com）




---


作者简介：


Joël Krähemann - 精通 C 语言编程的自由软件爱好者。不管代码多复杂，它也是一点点写成的。作为高级的 Gtk+ 程序开发者，我知道多线程编程有多大的挑战性，有了多线程编程，我们就有了未来需求的良好基础。


摘自: <https://opensource.com/article/17/7/functional-testing>


作者：[Joël Krähemann](https://opensource.com/users/joel2001k) 译者：[sugarfillet](https://github.com/sugarfillet) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
Automated tests are required to ensure your program's quality and that it works as expected. Unit tests examine only certain parts of your algorithm, but don't look at how each component fits together. That's where functional testing, sometimes referred as integration testing, comes in.

A functional test basically interacts with your user interface, whether through a website or a desktop application. To show you how that works, let's look at how to test a Gtk+ application. For simplicity, in this tutorial let's use the [Tictactoe](https://developer.gnome.org/gtk-tutorial/stable/) example from the Gtk+ 2.0 tutorial.

## Basic setup

For every functional test, you usually define some global variables, such as "user interaction delay" or "timeout until a failure is indicated" (i.e., when an event doesn't occur until the specified time and the application is doomed).

```
``````
#define TTT_FUNCTIONAL_TEST_UTIL_IDLE_CONDITION(f) ((TttFunctionalTestUtilIdleCondition)(f))
#define TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME (125000)
#define TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME_LONG (500000)
typedef gboolean (*TttFunctionalTestUtilIdleCondition)(gpointer data);
struct timespec ttt_functional_test_util_default_timeout = {
20,
0,
};
```


Now we can implement our dead-time functions. Here, we'll use the **usleep** function in order to get the desired delay.

```
``````
void
ttt_functional_test_util_reaction_time()
{
usleep(TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME);
}
void
ttt_functional_test_util_reaction_time_long()
{
usleep(TTT_FUNCTIONAL_TEST_UTIL_REACTION_TIME_LONG);
}
```


The timeout function delays execution until a state of a control is applied. It is useful for actions that are applied asynchronously, and that is why it delays for a longer period of time.

```
``````
void
ttt_functional_test_util_idle_condition_and_timeout(
TttFunctionalTestUtilIdleCondition idle_condition,
struct timespec *timeout,
pointer data)
{
struct timespec start_time, current_time;
clock_gettime(CLOCK_MONOTONIC,
&start_time);
while(TTT_FUNCTIONAL_TEST_UTIL_IDLE_CONDITION(idle_condition)(data)){
ttt_functional_test_util_reaction_time();
clock_gettime(CLOCK_MONOTONIC,
¤t_time);
if(start_time.tv_sec + timeout->tv_sec < current_time.tv_sec){
break;
}
}
ttt_functional_test_util_reaction_time();
}
```


## Interacting with the graphical user interface

In order to simulate user interaction, the [ Gdk library](https://developer.gnome.org/gdk3/stable/) provides the functions we need. To do our work here, we need only these three functions:

- gdk_display_warp_pointer()
- gdk_test_simulate_button()
- gdk_test_simulate_key()

For instance, to test a button click, we do the following:

```
``````
gboolean
ttt_functional_test_util_button_click(GtkButton *button)
{
GtkWidget *widget;
GdkWindow *window;
gint x, y;
gint origin_x, origin_y;
if(button == NULL ||
!GTK_IS_BUTTON(button)){
return(FALSE);
}
widget = button;
if(!GTK_WIDGET_REALIZED(widget)){
ttt_functional_test_util_reaction_time_long();
}
/* retrieve window and pointer position */
gdk_threads_enter();
window = gtk_widget_get_window(widget);
x = widget->allocation.x + widget->allocation.width / 2.0;
y = widget->allocation.y + widget->allocation.height / 2.0;
gdk_window_get_origin(window, &origin_x, &origin_y);
gdk_display_warp_pointer(gtk_widget_get_display(widget),
gtk_widget_get_screen(widget),
origin_x + x, origin_y + y);
gdk_threads_leave();
/* click the button */
ttt_functional_test_util_reaction_time();
gdk_test_simulate_button(window,
x,
y,
1,
GDK_BUTTON1_MASK,
GDK_BUTTON_PRESS);
ttt_functional_test_util_reaction_time();
gdk_test_simulate_button(window,
x,
y,
1,
GDK_BUTTON1_MASK,
GDK_BUTTON_RELEASE);
ttt_functional_test_util_reaction_time();
ttt_functional_test_util_reaction_time_long();
return(TRUE);
}
```


We want to ensure the button has an active state, so we provide an idle-condition function:

```
``````
gboolean
ttt_functional_test_util_idle_test_toggle_active(
GtkToggleButton **toggle_button)
{
gboolean do_idle;
do_idle = TRUE;
gdk_threads_enter();
if(*toggle_button != NULL &&
GTK_IS_TOGGLE_BUTTON(*toggle_button) &&
gtk_toggle_button_get_active(*toggle_button)){
do_idle = FALSE;
}
gdk_threads_leave();
return(do_idle);
}
```


## The test scenario

Since the Tictactoe program is very simple, we just need to ensure that a [ GtkToggleButton](https://developer.gnome.org/gtk3/stable/GtkToggleButton.html) was clicked. The functional test can proceed once it asserts the button entered the active state. To click the buttons, we use the handy

**util**function provided above.

For illustration, let's assume player A wins immediately by filling the very first row, because player B is not paying attention and just filled the second row:

```
``````
GtkWindow *window;
Tictactoe *ttt;
void*
ttt_functional_test_gtk_main(void *)
{
gtk_main();
pthread_exit(NULL);
}
void
ttt_functional_test_dumb_player_b()
{
GtkButton *buttons[3][3];
guint i;
/* to avoid race-conditions copy the buttons */
gdk_threads_enter();
memcpy(buttons, ttt->buttons, 9 * sizeof(GtkButton *));
gdk_threads_leave();
/* TEST 1 - the dumb player B */
for(i = 0; i < 3; i++){
/* assert player A clicks the button successfully */
if(!ttt_functional_test_util_button_click(buttons[0][i])){
exit(-1);
}
functional_test_util_idle_condition_and_timeout(
ttt_functional_test_util_idle_test_toggle_active,
ttt_functional_test_util_default_timeout,
&buttons[0][i]);
/* assert player B clicks the button successfully */
if(!ttt_functional_test_util_button_click(buttons[1][i])){
exit(-1);
}
functional_test_util_idle_condition_and_timeout(
ttt_functional_test_util_idle_test_toggle_active,
ttt_functional_test_util_default_timeout,
&buttons[1][i]);
}
}
int
main(int argc, char **argv)
{
pthread_t thread;
gtk_init(&argc, &argv);
/* start the tictactoe application */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
ttt = tictactoe_new();
gtk_container_add(window, ttt);
gtk_widget_show_all(window);
/* start the Gtk+ dispatcher */
pthread_create(&thread, NULL,
ttt_functional_test_gtk_main, NULL);
/* launch test routines */
ttt_functional_test_dumb_player_b();
/* terminate the application */
gdk_threads_enter();
gtk_main_quit();
gdk_threads_leave();
return(0);
}
```


## 2 Comments