Go-DOM - 用 Go 编写的无头浏览器

go-dom - 用 go 编写的无头浏览器

无事可做有时会导致疯狂的想法,而这一次;目的是通过嵌入 v8 引擎,用 go 编写一个无头浏览器,具有完整的 dom 实现和 javascript 支持。

这一切都是从编写 htmx 应用程序开始的,测试它的需要让我很好奇是否有无头浏览器的纯 go 实现。

搜索“go headless browser”只会导致搜索结果谈论自动化无头浏览器,即在无头模式下使用真正的浏览器,例如 firefox chrome

但是在纯 go 中什么都没有。

所以我开始建造一个。

为什么 go 中使用无头浏览器?

这可能看起来很愚蠢,因为编写无头浏览器永远不会像真正的浏览器一样工作;因此并不能真正验证您的应用程序是否在您决定支持的所有浏览器中正常工作。这也不允许您在停止工作时获得良好的功能,例如应用程序的屏幕截图。

那为什么呢?

极品速度!

为了在有效的 tdd 循环中工作,测试必须很快。缓慢的测试执行会阻碍 tdd,并且您会失去快速反馈循环提供的效率优势。

使用浏览器自动化进行此类验证会产生严重的开销,并且此类测试通常是在代码编写后编写;因此,它们不再有助于编写正确的实现;但事后却减少了维护负担;只是偶尔会在您的付费客户之前检测到错误。

目标是创建一个支持 tdd 流程的工具。为了可用,它需要在进程内运行。

需要用go编写。

减少不稳定的测试

dom 处于进程内可以在 dom 之上编写更好的包装器;这可以帮助为您的测试提供一个不太不稳定的界面,就像测试库为 javascript 所做的那样。

您不用依赖 css 类名、元素 id 或 dom 结构,而是使用以用户为中心的语言编写测试,如下所示。

在带有“电子邮件”标签的文本框中输入“me@example.com”

或者用假设的代码。

testing.getelement(query{
  role: "textbox",
  // the accessibility "name" of a textbox _is_ the label
  name: "email",
}).type("me@example.com")

此测试不关心标签是否实现为 for="...">、 https: target="_blank">ail"> 或

这将行为验证与 ui 更改解耦;但它确实强制文本“电子邮件”以可访问的方式与输入字段关联。这将测试与用户如何与页面交互结合起来;包括那些依赖屏幕阅读器使用您的页面的人。

这实现了tdd最重要的方面;编写与具体行为相结合的测试。1

虽然在技术上可能可以为进程外浏览器编写相同的测试;原生代码的好处对于这些类型的助手最可能需要的 dom 随机访问类型至关重要。

示例:javascript

为了举例说明测试类型,我将使用 javascript 中的类似示例;也是一个使用 htmx 的应用程序。该测试验证请求需要身份验证的页面的一般登录流程。

有点长,因为我在这里将所有设置和帮助程序代码合并到一个测试函数中。

it("Redirects to /local after a successful login", async () => {
  // Setup - stub the authentication, and create a stubbed user
  // using a test helper
  sinon
    .stub(auth, "authenticate")
    .withArgs({
      email: "jd@example.com",
      // matchPassword helper is used, as passwords are wrapped in a class
      // preventing accidental disclosure in logs, console out, etc.
      password: matchPassword("s3cret"),
    })
    .resolves(
      auth.AuthenticateResult.success(createUser({ firstName: "John" })),
    );
  const url = `http://127.0.0.1:${port}/auth/login?redirectUrl=%2Flocal`;
  // Request private page. This _should_ generate a redirect
  const wrapper = await DOMWrapper.open(url); // Just a helper around jsdom
  const browser = wrapper.browser;
  // Once HTMX is ready, it emits an `htmx:load` event. Then verify that it was 
  // correctly redirected.
  await wrapper.waitFor("htmx:load");
  expect(wrapper.url.pathname).to.equal("/auth/login");
  // Use testing-library to fill out and submit the form
  let screen = wrapper.screen;
  const username = screen.getByRole("textbox", { name: "Email" });
  const password = screen.getByLabelText("Password");
  await userEvent.type(username, "jd@example.com");
  await userEvent.type(password, "s3cret"); // password has no role
  // Wait for a new `htmx:load` event, while clicking the submit button
  // at the same time.
  await wrapper.runAndWaitFor(
    ["htmx:load"],
    userEvent.click(screen.getByRole("button", { name: "Sign in" })),
  );
  // After the new new page has been loaded, verify that the username
  // is displayed (i.e. the stubbed user is used), and the correct
  // URL is used.
  screen = testingLibrary.within(browser.window.document.body);
  const heading = screen.getByRole("heading", { level: 1 });
  expect(heading.innerHTML).to.equal("Hi, John");
  expect(wrapper.url.pathname).to.equal("/local");
});

简单来说,测试执行以下操作:

  1. 删除身份验证函数,模拟成功的响应。
  2. 请求需要身份验证的页面
  3. 验证浏览器是否重定向到登录页面,并且浏览器 url 是否已更新。 2
  4. 在表格中填写预期值,然后提交。
  5. 验证浏览器是否重定向到最初请求的页面,并且它显示了存根用户的信息。

测试在内部启动 http 服务器。因为 this 在测试过程中运行,所以可以对业务逻辑进行模拟和存根。测试使用jsdom与http服务器通信;它既将 html 响应解析为 dom,又在已初始化的沙箱中执行客户端脚本,例如以 window 作为全局范围。3

这使得能够编写 http 层的测试,其中验证响应的内容是不够的。在这种情况下; htmx 按预期处理响应。

但是除了等待一些 htmx 事件,以免过早(或太晚)进行之外,测试实际上并不关心 htmx。事实上,如果我从表单中删除 htmx,采用经典重定向,测试仍然可以通过。

(如果我完全删除 htmx

速度?查看!

虽然之前的测试比预期慢了一点;它相当快,通常在 150-180 毫秒内完成。对于大多数测试套件来说这太慢了,但它足够快,可以在处理该特定功能时充当反馈循环。

此测试不是正常 tdd 运行的一部分。当我处理该功能时,它们就会运行;或在提交之前;确保没有任何损坏。这是处理“慢测试”的完全正常的方式。

潜在的速度改进

javascript 示例使用在随机端口上启动的真实 http 服务器。服务器在测试运行器的进程中运行,这就是为什么我们可以存根和模拟业务逻辑。

在 go 中,http 请求由 http.handler 处理,因此无需实际启动 http 服务器即可轻松使用 http 处理逻辑。

这是 go-dom 代码 现在 处理的事情,目前测试套件的运行时间为零毫秒,四舍五入到最接近的毫秒。4

模拟和并行测试?

运行并行测试的能力仅取决于您的代码并行运行的能力。由于这可以消耗 http.handler,因此每个测试都可以创建自己的处理程序;每个都有不同的依赖项,替换为测试双倍,以适合单独的测试。

这允许您测试整个 http 层;使用存根业务逻辑。

项目现状?

几乎没有任何实施;目前的状态是大约一天半工作的结果。我有一个基本的流标记生成器,可以使用 http 响应流,该响应流被传递到返回 node 的解析器。

代码当前可以将字符串 (尚不允许空格)处理为 htmlhtmlelement。

接下来的步骤是

  • 稍微改进解析器,并实现更多元素类型
  • 嵌入v8引擎,解决主要不确定性 javascript 如何访问 go 对象,以及 go 代码如何检查 javascript 代码的突变结果。
    • 我使用 v8go 制作了一个极其简单的 poc,以及似乎维护得最好的 fork。它缺乏必要的功能;我已将其添加到我自己的叉子中

该项目的未来?

这很可能会死:(

我什至没有在 go 项目上工作,这将是有价值的(我当时在 node.js 项目上工作)。很高兴看到 jsdom 如何帮助充当身份验证流程的反馈循环,从而激发了一个有趣的愚蠢想法。作为一个患有多动症的人,这对我来说是一个典型的模式。我开始做一些有趣的事情,并努力去做;直到有其他东西引起我的注意并引起我的兴趣。

除非...

其他开发人员认为这是一个好主意,并希望帮助构建它。

我相信这样的工具对于任何将服务器端渲染与客户端脚本相结合的 go 项目都非常有帮助,包括基于 htmx 的应用程序。

该项目可以在这里找到:https://github.com/stroiman/go-dom


  1. tdd 的目标是而不是编写单元测试。这是一个极其常见的现象;但完全不正确的误解。 ↩

  2. 重要的不是我们被重定向;而是我们被重定向了。但浏览器历史记录具有正确的条目,提供浏览器后退/前进功能的合理行为。测试应该确实验证了历史的内容,或者甚至可能主动使用导航api来来回。像这样;该测试在描述从用户角度的预期行为方面还可以改进。 ↩

  3. javascript 编写无头浏览器具有不公平的优势;因为你的模拟 dom 已经是有效的 javascript 对象。在 go 中,需要额外的工作来允许客户端脚本改变 dom,并让结果可以在测试代码中访问。 ↩

  4. 据 ginkgo 报道。这不包括构建和启动的开销,这有一个明显但非常短的延迟。 ↩

以上就是Go-DOM - 用 Go 编写的无头浏览器的详细内容,更多请关注其它相关文章!