Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Prev Previous commit
Next Next commit
Add real tests, and make them build.
  • Loading branch information
stuartmorgan-g committed Jul 14, 2021
commit bc94f32468490275bab4165302be946ab27af8bc
35 changes: 26 additions & 9 deletions packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ project(${PROJECT_NAME} LANGUAGES CXX)

set(PLUGIN_NAME "${PROJECT_NAME}_plugin")

add_library(${PLUGIN_NAME} SHARED
list(APPEND PLUGIN_SOURCES
"system_apis.cpp"
"system_apis.h"
"url_launcher_plugin.cpp"
"url_launcher_plugin.h"
)

add_library(${PLUGIN_NAME} SHARED
"url_launcher_plugin_registration.cpp"
${PLUGIN_SOURCES}
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter_wrapper_plugin)

# List of absolute paths to libraries that should be bundled with the plugin
set(file_chooser_bundled_libraries
Expand All @@ -24,7 +31,7 @@ set(file_chooser_bundled_libraries
# === Tests ===

if (${include_${PROJECT_NAME}_tests})
set(TEST_RUNNER_NAME "${PROJECT_NAME}_test")
set(TEST_RUNNER "${PROJECT_NAME}_test")
enable_testing()
# TODO(stuartmorgan): Use a single shared, pre-checked-in googletest instance
# rather than downloading for each plugin. This approach makes sense for a
Expand All @@ -41,12 +48,22 @@ set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)

FetchContent_MakeAvailable(googletest)

add_executable(${TEST_RUNNER_NAME}
# The plugin's C API is not very useful for unit testing, so build the sources
# directly into the test binary rather than using the DLL.
add_executable(${TEST_RUNNER}
test/url_launcher_windows_test.cpp
${PLUGIN_SOURCES}
)
target_link_libraries(${TEST_RUNNER_NAME}
gtest_main
apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FLUTTER_LIBRARY}" $<TARGET_FILE_DIR:${TEST_RUNNER}>
)

include(GoogleTest)
gtest_discover_tests(${TEST_RUNNER_NAME})
gtest_discover_tests(${TEST_RUNNER})
endif()
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ namespace url_launcher_plugin {

SystemApis::SystemApis() {}

SystemApis::~SystemApis() {}

SystemApisImpl::SystemApisImpl() {}

SystemApisImpl::~SystemApisImpl() {}

LSTATUS SystemApisImpl::RegCloseKey(HKEY key) { return ::RegCloseKey(key); }

LSTATUS SystemApisImpl::RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,170 @@
#include <flutter/method_call.h>
#include <flutter/method_result_functions.h>
#include <flutter/standard_method_codec.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <windows.h>

TEST(TestMe, HelloTestWorld) {
EXPECT_STRNE("hello", "world");
EXPECT_TRUE(false);
#include <memory>
#include <string>

#include "url_launcher_plugin.h"

namespace url_launcher_plugin {
namespace test {

namespace {

using flutter::EncodableMap;
using flutter::EncodableValue;
using ::testing::DoAll;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::SetArgPointee;

class MockSystemApis : public SystemApis {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicky and up to you -- I wonder if a simple fake here that returns hardcoded/deterministic values and maybe a bool for open/close state would be enough, and make the tests themselves a bit more readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The results can't be hard-coded, because I wanted to do full failure path testing, which means I would need a variable and a setter for the return value of each method. By the time I did that, the helper would be a lot more complicated than the mock declaration, so I came down on the side of gmock. (This also lets me check a few arguments to the calls trivially, which is a nice bit of added test robustness.)

public:
MOCK_METHOD(LSTATUS, RegCloseKey, (HKEY key), (override));
MOCK_METHOD(LSTATUS, RegQueryValueExW,
(HKEY key, LPCWSTR value_name, LPDWORD type, LPBYTE data,
LPDWORD data_size),
(override));
MOCK_METHOD(LSTATUS, RegOpenKeyExW,
(HKEY key, LPCWSTR sub_key, DWORD options, REGSAM desired,
PHKEY result),
(override));
MOCK_METHOD(HINSTANCE, ShellExecuteW,
(HWND hwnd, LPCWSTR operation, LPCWSTR file, LPCWSTR parameters,
LPCWSTR directory, int show_flags),
(override));
};

class MockMethodResult : public flutter::MethodResult<> {
public:
MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result),
(override));
MOCK_METHOD(void, ErrorInternal,
(const std::string& error_code, const std::string& error_message,
const EncodableValue* details),
(override));
MOCK_METHOD(void, NotImplementedInternal, (), (override));
};

std::unique_ptr<EncodableValue> CreateArgumentsWithUrl(const std::string& url) {
EncodableMap args = {
{EncodableValue("url"), EncodableValue(url)},
};
return std::make_unique<EncodableValue>(args);
}

} // namespace

TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
std::unique_ptr<MockMethodResult> result =
std::make_unique<MockMethodResult>();

// Return success values from the registery commands.
HKEY fake_key = reinterpret_cast<HKEY>(1);
EXPECT_CALL(*system, RegOpenKeyExW)
.WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS)));
EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS));
EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS));
// Expect a success response.
EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true))));

UrlLauncherPlugin plugin(std::move(system));
plugin.HandleMethodCall(
flutter::MethodCall(
"canLaunch", CreateArgumentsWithUrl("https://some.url.com")

),
std::move(result));
}

TEST(UrlLauncherPlugin, CanLaunchQueryFailure) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
std::unique_ptr<MockMethodResult> result =
std::make_unique<MockMethodResult>();

// Return success values from the registery commands, except for the query,
// to simulate a scheme that is in the registry, but has no URL handler.
HKEY fake_key = reinterpret_cast<HKEY>(1);
EXPECT_CALL(*system, RegOpenKeyExW)
.WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS)));
EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND));
EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS));
// Expect a success response.
EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false))));

UrlLauncherPlugin plugin(std::move(system));
plugin.HandleMethodCall(
flutter::MethodCall(
"canLaunch", CreateArgumentsWithUrl("https://some.url.com")

),
std::move(result));
}

TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
std::unique_ptr<MockMethodResult> result =
std::make_unique<MockMethodResult>();

// Return failure for opening.
EXPECT_CALL(*system, RegOpenKeyExW)
.WillOnce(Return(ERROR_BAD_PATHNAME));
// Expect a success response.
EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false))));

UrlLauncherPlugin plugin(std::move(system));
plugin.HandleMethodCall(
flutter::MethodCall(
"canLaunch", CreateArgumentsWithUrl("https://some.url.com")

),
std::move(result));
}

TEST(UrlLauncherPlugin, LaunchSuccess) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
std::unique_ptr<MockMethodResult> result =
std::make_unique<MockMethodResult>();

// Return a success value (>32) from launching.
EXPECT_CALL(*system, ShellExecuteW)
.WillOnce(Return(reinterpret_cast<HINSTANCE>(33)));
// Expect a success response.
EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true))));

UrlLauncherPlugin plugin(std::move(system));
plugin.HandleMethodCall(
flutter::MethodCall(
"launch", CreateArgumentsWithUrl("https://some.url.com")

),
std::move(result));
}

TEST(UrlLauncherPlugin, LaunchReportsFailure) {
std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
std::unique_ptr<MockMethodResult> result =
std::make_unique<MockMethodResult>();

// Return a faile value (<=32) from launching.
EXPECT_CALL(*system, ShellExecuteW)
.WillOnce(Return(reinterpret_cast<HINSTANCE>(32)));
// Expect an error response.
EXPECT_CALL(*result, ErrorInternal);

UrlLauncherPlugin plugin(std::move(system));
plugin.HandleMethodCall(
flutter::MethodCall(
"launch", CreateArgumentsWithUrl("https://some.url.com")

),
std::move(result));
}

} // namespace test
} // namespace url_launcher_plugin
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
#include <sstream>
#include <string>

#include "include/url_launcher_windows/url_launcher_plugin.h"

namespace url_launcher_plugin {

namespace {
Expand Down Expand Up @@ -68,8 +66,8 @@ void UrlLauncherPlugin::RegisterWithRegistrar(
registrar->messenger(), "plugins.flutter.io/url_launcher",
&flutter::StandardMethodCodec::GetInstance());

// Uses new instead of make_unique due to private constructor.
std::unique_ptr<UrlLauncherPlugin> plugin(new UrlLauncherPlugin());
std::unique_ptr<UrlLauncherPlugin> plugin =
std::make_unique<UrlLauncherPlugin>();

channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result) {
Expand Down Expand Up @@ -155,10 +153,3 @@ std::optional<std::string> UrlLauncherPlugin::LaunchUrl(
}

} // namespace url_launcher_plugin

void UrlLauncherPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@ class UrlLauncherPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar);

virtual ~UrlLauncherPlugin();

// Disallow copy and move.
UrlLauncherPlugin(const UrlLauncherPlugin&) = delete;
UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete;

private:
UrlLauncherPlugin();

// Creates a plugin instance with the given SystemApi instance.
//
// Exists for unit testing with mock implementations.
UrlLauncherPlugin(std::unique_ptr<SystemApis> system_apis);

// Called when a method is called on plugin channel;
virtual ~UrlLauncherPlugin();

// Disallow copy and move.
UrlLauncherPlugin(const UrlLauncherPlugin&) = delete;
UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete;

// Called when a method is called on the plugin channel.
void HandleMethodCall(const flutter::MethodCall<>& method_call,
std::unique_ptr<flutter::MethodResult<>> result);

private:
// Returns whether or not the given URL has a registered handler.
bool CanLaunchUrl(const std::string& url);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <flutter/plugin_registrar_windows.h>

#include "include/url_launcher_windows/url_launcher_plugin.h"
#include "url_launcher_plugin.h"

void UrlLauncherPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}