Google Chrome の UI テスト (実装編) (2)

2009-01-11 15:38

GUI をもつソフトウェアのテストに関心があるので Google Chrome というか Chromium の UI テストをみていた。

Chromium TestShell

ビルドと実行

Chromium の UI テストはいまのところ Mac では試せない。実行はできるものの、テストケースが含まれていない。

% ./xcodebuild/Debug/ui_tests
[==========] Running 0 tests from 0 test cases.
[==========] 0 tests from 0 test cases ran.
[  PASSED  ] 0 tests.
%

Windows でも Express じゃない Visual Studio が必要 なので、淡々とソースを読んだ。

ファイル

Chromium には testing/ とか chrome/test/ とかそれっぽいフォルダがある。testing/ には Google C++ Testing Framework (以下、Chromium Developer Documentation の Testing のページ にならって gtest と略す) が、chrome/test/ には後述する AutomationProxy などテストにだけ使うコードやファイルが置かれている。実際の個々のコードに対するテストコードは、元コードと同じフォルダにおく流儀のようだ。

% ls -d **/*unittest*.c* | wc -l
     312
% ls -d **/*uitest*.c* | wc -l
      36
% ls -d **/*uitest*.c*
chrome/app/chrome_main_uitest.cc
chrome/browser/browser_focus_uitest.cc
chrome/browser/browser_uitest.cc
chrome/browser/crash_recovery_uitest.cc
chrome/browser/download/download_uitest.cc
chrome/browser/download/save_page_uitest.cc
chrome/browser/errorpage_uitest.cc
chrome/browser/history/redirect_uitest.cc
chrome/browser/iframe_uitest.cc
chrome/browser/images_uitest.cc
chrome/browser/interstitial_page_uitest.cc
chrome/browser/locale_tests_uitest.cc
chrome/browser/login_prompt_uitest.cc
chrome/browser/metrics_service_uitest.cc
chrome/browser/printing/printing_layout_uitest.cc
chrome/browser/renderer_host/resource_dispatcher_host_uitest.cc
chrome/browser/sanity_uitest.cc
chrome/browser/session_history_uitest.cc
chrome/browser/sessions/session_restore_uitest.cc
chrome/browser/ssl_uitest.cc
chrome/browser/tab_restore_uitest.cc
chrome/browser/unload_uitest.cc
chrome/browser/view_source_uitest.cc
chrome/browser/views/constrained_window_impl_interactive_uitest.cc
chrome/browser/views/find_bar_win_interactive_uitest.cc
chrome/browser/views/find_bar_win_uitest.cc
chrome/common/logging_chrome_uitest.cc
chrome/common/net/cache_uitest.cc
chrome/common/pref_service_uitest.cc
chrome/test/automation/automation_proxy_uitest.cc
chrome/test/ui/history_uitest.cc
chrome/test/ui/inspector_controller_uitest.cc
chrome/test/ui/layout_plugin_uitest.cpp
chrome/test/ui/npapi_uitest.cpp
chrome/test/ui/omnibox_uitest.cc
chrome/test/ui/sandbox_uitests.cc
%

chrome/app/chrome_main_uitest.cc

とりあえず先頭にあった chrome_main_uitest.cc を読んでみる。

typedef UITest ChromeMainTest;

// Launch the app, then close the app.
TEST_F(ChromeMainTest, AppLaunch) {
  // If we make it here at all, we've succeeded in retrieving the app window
  // in UITest::SetUp()--otherwise we'd fail with an exception in SetUp().

  if (UITest::in_process_renderer()) {
    EXPECT_EQ(1, UITest::GetBrowserProcessCount());
  } else {
    // We should have two instances of the browser process alive -
    // one is the Browser and the other is the Renderer.
    EXPECT_EQ(2, UITest::GetBrowserProcessCount());
  }
}

TEST_F, EXPECT_EQ あたりは gtest でおなじみだろう。TEST_F の第一引数になっている ChromeMainTest == UITest は gtest の testing::Test を継承している。

in_process_renderer() は変数の値を返すだけ (chrome/test/ui/ui_test.h, chrome/test/ui/ui_test.cc) だし、GetBrowserProcessCount() は OS にプロセス数を問い合わせる (chrome/test/ui/ui_test.h, base/process_util_win.cc) だけだ。

次のテストはさらに短い。

// Make sure that the testing interface is there and giving reasonable answers.
TEST_F(ChromeMainTest, AppTestingInterface) {
  int window_count = 0;
  EXPECT_TRUE(automation()->GetBrowserWindowCount(&window_count));
  EXPECT_EQ(1, window_count);

  EXPECT_EQ(1, GetTabCount());
}

が、内側の仕組みはだいぶ複雑になっている。

AutomationProxy

いままでちゃんと見ていなかったけど、実は一連のテストはブラウザとは別のプロセスで走っている。

void UITest::SetUp() {
  if (!use_existing_browser_) {
    AssertAppNotRunning(L"Please close any other instances "
                        L"of the app before testing.");
  }

  InitializeTimeouts();
  LaunchBrowserAndServer();
}

...

void UITest::LaunchBrowserAndServer() {
#if defined(OS_WIN)
  // Set up IPC testing interface server.
  server_.reset(new AutomationProxy(command_execution_timeout_ms_));

  LaunchBrowser(launch_arguments_, clear_profile_);
  if (wait_for_initial_loads_)
    ASSERT_TRUE(server_->WaitForInitialLoads());
  else
    Sleep(2000);

  automation()->SetFilteredInet(true);
#else
  // TODO(port): depends on AutomationProxy.
  NOTIMPLEMENTED();
#endif
}

別プロセスであるブラウザにウィンドウ数などを問い合わせるためのクラスが AutomationProxy だ。

server_.reset(new AutomationProxy(…)) は server_ がいわゆるスマートポインタ (base/scoped_ptr.h) なのでこうなっている。おおまかにいえば server_ = new AutomationProxy(…) だ。

automation() は server_ のさす AutomationProxy のインスタンスを返して (chrome/test/ui/ui_test.h) いる。

実際に GetBrowserWindowCount をみてみよう。

bool AutomationProxy::GetBrowserWindowCount(int* num_windows) {
  if (!num_windows) {
    NOTREACHED();
    return false;
  }

  IPC::Message* response = NULL;
  bool is_timeout = true;
  bool succeeded = SendAndWaitForResponseWithTimeout(
      new AutomationMsg_BrowserWindowCountRequest(0), &response,
      AutomationMsg_BrowserWindowCountResponse::ID,
      command_execution_timeout_ms_, &is_timeout);
  if (!succeeded)
    return false;

  if (is_timeout) {
    DLOG(ERROR) << "GetWindowCount did not complete in a timely fashion";
    return false;
  }

  void* iter = NULL;
  if (!response->ReadInt(&iter, num_windows)) {
    succeeded = false;
  }

  delete response;
  return succeeded;
}

AutomationMsg_BrowserWindowCountRequest, AutomationMsg_BrowserWindowCountResponse::ID のあたりは chrome/test/automation/automation_messages_internal.h で延々と定義されている。

// By using a start value of 0 for automation messages, we keep backward
// compatability with old builds.
IPC_BEGIN_MESSAGES(Automation, 0)
  ...
  // This message requests the number of browser windows that the app currently
  // has open.  The parameter in the response is the number of windows.
  IPC_MESSAGE_ROUTED0(AutomationMsg_BrowserWindowCountRequest)
  IPC_MESSAGE_ROUTED1(AutomationMsg_BrowserWindowCountResponse, int)
  ...
IPC_END_MESSAGES(Automation)

お、リクエストとレスポンスが別々だ。

ちなみに automation_messages_internal.h には多重読み込みをふせぐ #ifndef なガードが無くて、automation_messages.h で

// Two-pass include of render_messages_internal.  Preprocessor magic allows
// us to use 1 header to define the enums and classes for our render messages.
#define IPC_MESSAGE_MACROS_ENUMS
#include "chrome/test/automation/automation_messages_internal.h"

#ifdef IPC_MESSAGE_MACROS_LOG_ENABLED
#  undef IPC_MESSAGE_MACROS_LOG
#  define IPC_MESSAGE_MACROS_CLASSES
#include "chrome/test/automation/automation_messages_internal.h"

#  undef IPC_MESSAGE_MACROS_CLASSES
#  define IPC_MESSAGE_MACROS_LOG
#include "chrome/test/automation/automation_messages_internal.h"
#else
// No debug strings requested, just define the classes
#  define IPC_MESSAGE_MACROS_CLASSES
#include "chrome/test/automation/automation_messages_internal.h"
#endif

と3回も include している。じつは chrome/common/ipc_message_macros.h では IPC_MESSAGE_ROUTEn も3回定義されていて、ここらへんプリプロセッサ愛好家には見物かもしれない。

閑話休題。AutomationProxy::SendAndWaitForResponseWithTimeout は AutomationProxy::Send から
Channel::Send, Channel::ChannelImpl::Send とすすむ。

bool Channel::ChannelImpl::Send(Message* message) {
  chrome::Counters::ipc_send_counter().Increment();
#ifdef IPC_MESSAGE_DEBUG_EXTRA
  DLOG(INFO) << "sending message @" << message << " on channel @" << this
             << " with type " << message->type()
             << " (" << output_queue_.size() << " in queue)";
#endif

#ifdef IPC_MESSAGE_LOG_ENABLED
  Logging::current()->OnSendMessage(message, L"");
#endif

  output_queue_.push(message);
  // ensure waiting to write
  if (!waiting_connect_) {
    if (!output_state_.is_pending) {
      if (!ProcessOutgoingMessages(NULL, 0))
        return false;
    }
  }

  return true;
}

...

bool Channel::ChannelImpl::ProcessOutgoingMessages(
    MessageLoopForIO::IOContext* context,
    DWORD bytes_written) {
  ...

  // Write to pipe...
  Message* m = output_queue_.front();
  BOOL ok = WriteFile(pipe_,
                      m->data(),
                      m->size(),
                      &bytes_written,
                      &output_state_.context.overlapped);
  if (!ok) {
    DWORD err = GetLastError();
    if (err == ERROR_IO_PENDING) {
      output_state_.is_pending = true;

#ifdef IPC_MESSAGE_DEBUG_EXTRA
      DLOG(INFO) << "sent pending message @" << m << " on channel @" <<
                    this << " with type " << m->type();
#endif

      return true;
    }
    LOG(ERROR) << "pipe error: " << err;
    return false;
  }

  ...
}

Win32 API の WriteFile でプロセス間の通信を行っているようだ。

まとめ

設計面の関心からはじまったので、本当は個々のテストを見るべきなんだけど、なぜか実装を追いすぎました。深さじゃなくて幅優先するべきだった。

仕組みとしてはかなりごついので、すぐさま自分のプロジェクトに同等の仕組みを用意するのは大変そうだ。ただ、ごつさは C++ の所為もある気がしていて、リフレクション系の機能がある言語ならもっとさっぱりするかもしれない。

テストは投資に対するリターンが良いので、ごつくてもやるべきですかそうですか。

morita

リフレクションある系の言語や win32 でない GUI framework では、
大抵 GUI テストの自動化ツールがオープンソースである気がします。
あと chrome がごついのは C++ のせいの他に、マルチプロセスのアーキテクチャだからというのはありそうです。
ライブラリなら関数を呼べば済むわけで・・・
この例だけみると GUI のテストというよりサーバプログラムのテストですね。

kzys

「Google といえばテスト」で見たんですが、たしかに Chrome は特殊だったかも。

Win32 でないものを探したら Dogtail, QTestLib とかみつけました。Dogtail はアクセシビリティ API つかってて粒度おおきめ、QTestLib はイベント送ったり signal/slot 使ったりで粒度小さめ。

あと PragProg から “Scripted GUI Testing with Ruby” なんてものがでてました。
http://www.pragprog.com/titles/idgtr/scripted-gui-testing-with-ruby

Leave a Reply