React + Webpack + Rails API 站点的集成测试选项有哪些?

What are the options for integration testing of React + Webpack + Rails API site?

考虑一个站点,其中 Rails 仅用于 API。没有服务器端渲染。

通过服务器端渲染,它或多或少是清晰的。 capybara 启动 puma,之后测试可以连接到 puma 页面。

但是没有服务器端呈现,就没有 puma 请求页面。我该怎么做?

在投票时请自己解释

看看http://ruby-hyperloop.org。您可以从 rspec 驱动您的客户端测试套件,并轻松与 rails

集成

虽然现在服务器端渲染一定很普遍,但我决定 take an alternative approach

将以下宝石添加到 Gemfile

gem 'httparty', '~> 0.16.2'
gem 'childprocess', '~> 0.7.0'

将以下行从 config/environments/production.rb 移动到 config/application.rb 以使 RAILS_LOG_TO_STDOUT 在测试环境中可用。

if ENV['RAILS_LOG_TO_STDOUT'].present?
  config.logger = Logger.new(STDOUT)
end

关于webpack,确保publicPath设置为http://localhost:7777/,并且test environment中没有使用UglifyJsPlugin

并添加这两个文件:

test/application_system_test_case.rb:

# frozen_string_literal: true

require 'uri'
require 'test_helper'
require 'front-end-server'

FRONT_END = ENV.fetch('FRONT_END', 'separate_process')
FRONT_END_PORT = 7777

Capybara.server_port = 7778
Capybara.run_server = ENV.fetch('BACK_END', 'separate_process') == 'separate_thread'
require 'action_dispatch/system_test_case'   # force registering and setting server
Capybara.register_server :rails_puma do |app, port, host|
  Rack::Handler::Puma.run(app, Port: port, Threads: "0:1",
    Verbose: ENV.key?('BACK_END_LOG'))
end
Capybara.server = :rails_puma

DatabaseCleaner.strategy = :truncation

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
  self.use_transactional_tests = false

  def setup
    DatabaseCleaner.start
  end

  def teardown
    DatabaseCleaner.clean
  end

  def uri(path)
    URI::HTTP.build(host: 'localhost', port: FRONT_END_PORT, path: path)
  end
end

unless ENV.key?('NO_WEBPACK')
  system(
    {'NODE_ENV' => 'test'},
    './node_modules/.bin/webpack', '--config', 'config/webpack/test.js', '--hide-modules') \
    or abort
end

if FRONT_END == 'separate_process'
  front_srv = ChildProcess.build(
    'bundle', 'exec', 'test/front-end-server.rb',
    '-f', FRONT_END_PORT.to_s,
    '-b', Capybara.server_port.to_s
  )
  if ENV.key?('FRONT_END_LOG')
    front_srv.io.inherit!
  end
  front_srv.start
  Minitest.after_run {
    front_srv.stop
  }
else
  Thread.new do
    FrontEndServer.new({
      Port: FRONT_END_PORT,
      back_end_port: Capybara.server_port,
      Logger: Rails.logger,
    }).start
  end
end

unless Capybara.run_server
  back_srv = ChildProcess.build(
    'bin/rails', 'server',
    '-P', 'tmp/pids/server-test.pid',   # to not conflict with dev instance
    '-p', Capybara.server_port.to_s
  )
  back_srv.start

  # wait for server to start
  begin
    socket = TCPSocket.new 'localhost', Capybara.server_port
  rescue Errno::ECONNREFUSED
    retry
  end
  socket.close

  Minitest.after_run {
    back_srv.stop
  }
end

test/front-end-server.rb:

#!/usr/bin/env ruby
require 'webrick'
require 'httparty'
require 'uri'

class FrontEndServer < WEBrick::HTTPServer
  class FallbackFileHandler < WEBrick::HTTPServlet::FileHandler
    def service(req, res)
      super
    rescue WEBrick::HTTPStatus::NotFound
      req.instance_variable_set('@path_info', '/index.html')
      super
    end
  end

  class ProxyHandler < WEBrick::HTTPServlet::AbstractServlet
    def do_GET(req, res)
      req.header.each do |k, v|
        @logger.debug("-> #{k}: #{v}");
      end
      @logger.debug("-> body: #{req.body}");

      uri2 = req.request_uri.dup
      uri2.port = @config[:back_end_port]
      res2 = HTTParty.send(req.request_method.downcase, uri2, {
        headers: Hash[req.header.map { |k, v| [k, v.join(', ')] }],
        body: req.body,
      })
      res.content_type = res2.headers['content-type']
      res.body = res2.body

      res2.headers.each do |k, v|
        @logger.debug("<- #{k}: #{v}");
      end
      if res.body
        body = res.body.length < 100 ? res.body : res.body[0,97] + '...'
        @logger.debug("<- body: #{req.body}");
      end
    end
    alias do_POST do_GET
    alias do_PATCH do_GET
    alias do_PUT do_GET
    alias do_DELETE do_GET
    alias do_MOVE do_GET
    alias do_COPY do_GET
    alias do_HEAD do_GET
    alias do_OPTIONS do_GET
    alias do_MKCOL do_GET
  end

  def initialize(config={}, default=WEBrick::Config::HTTP)
    config = {AccessLog: config[:Logger] ? [
      [config[:Logger], WEBrick::AccessLog::COMMON_LOG_FORMAT],
    ] : [
      [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT],
    ]}.update(config)
    super
    if ENV.key?('FRONT_END_LOG_LEVEL')
      logger.level = WEBrick::BasicLog.const_get(ENV['FRONT_END_LOG_LEVEL'])
    end

    mount('/', FallbackFileHandler, 'public')
    mount('/api', ProxyHandler)
    mount('/uploads', ProxyHandler)
  end
end

if __FILE__ == [=13=]
  require 'optparse'

  options = {}
  OptionParser.new do |opt|
    opt.on('-f', '--front-end-port PORT', OptionParser::DecimalInteger) { |o|
      options[:front_end_port] = o
    }
    opt.on('-b', '--back-end-port PORT', OptionParser::DecimalInteger) { |o|
      options[:back_end_port] = o
    }
  end.parse!

  server = FrontEndServer.new({
    Port: options[:front_end_port],
    back_end_port: options[:back_end_port],
  })
  trap('INT') { server.shutdown }
  trap('TERM') { server.shutdown }
  server.start
end

使用 rails-5.1.1webpack-2.4.1 测试。

要运行测试你可以使用以下命令:

$ xvfb-run TESTOPTS=-h bin/rails test:system
$ xvfb-run bin/rails test -h test/system/application_test.rb:6
$ xvfb-run TEST=test/system/application_test.rb TESTOPTS=-h bin/rake test

您可以通过添加包脚本来简化 运行ning 测试:

"scripts": {
  "test": "xvfb-run bin/rails test:system",
  "test1": "xvfb-run bin/rails test"
}

然后:

$ yarn test
$ yarn test1 test/system/application_test.rb:6

或者我想说的。但不幸的是 yarn 有一个 issue ,它在 PATH 变量的前面加上额外的路径。特别是 /usr/bin。这导致系统 ruby 被执行。结果各式各样(ruby 没有找到宝石)。

要解决此问题,您可以使用以下脚本:

#!/usr/bin/env bash
set -eu

# https://github.com/yarnpkg/yarn/issues/5935

s_path=$(printf "%s" "$PATH" | tr : \n)
_IFS=$IFS
IFS=$'\n'
a_path=($s_path)
IFS=$_IFS

usr_bin=$(dirname -- "$(which node)")
n_usr_bin=$(egrep "^$usr_bin$" <(printf "%s" "$s_path") | wc -l)
r=()
for (( i = 0; i < ${#a_path[@]}; i++ )); do
    if [ "${a_path[$i]}" = "$usr_bin" ] && (( n_usr_bin > 1 )); then
        (( n_usr_bin-- ))
    else
        r+=("${a_path[$i]}")
    fi
done

PATH=$(
    for p in ${r[@]+"${r[@]}"}; do
        printf "%s\n" "$p"
    done | paste -sd:
)

"$@"

那么要读取的打包脚本如下:

"scripts": {
  "test": "./fix-path.sh xvfb-run bin/rails test:system",
  "test1": "./fix-path.sh xvfb-run bin/rails test"
}

默认情况下,rails 在单独的线程中启动 puma 以在 运行 测试时处理 api 请求。使用此设置,默认情况下 运行s 在一个单独的进程中。从那时起,您可以在测试中的任何位置放置 byebug 行,浏览器中的站点将保持正常运行(XHR 请求不会卡住)。如果您愿意,您仍然可以通过设置 BACK_END=separate_thread.

在单独的线程中创建它 运行

此外,另一个进程(或线程,取决于 FRONT_END 变量的值)将开始处理对静态文件的请求(或对后端的代理请求)。为此,使用 webrick

要查看 rails 的输出,运行 和 RAILS_LOG_TO_STDOUT=1,或查看 log/test.log。要防止 rails 对日志着色,请将 config.colorize_logging = false(也会在控制台中去除颜色)添加到 config/environments/test.rb,或使用 less -R log/test.logpuma的输出可以被运行 BACK_END_LOG=1.

看到

要查看 webrick 的输出,运行 使用 FRONT_END_LOG=1(单独的进程),RAILS_LOG_TO_STDOUT=1(单独的线程),或查看 log/test.log(单独的线程)。要使 webrick 产生更多信息,请将 FRONT_END_LOG_LEVEL 设置为 DEBUG

此外,每次 运行 测试时,webpack 都会开始编译包。您可以使用 WEBPACK=1.

来避免这种情况

终于看到了Selenium requests

Selenium::WebDriver.logger.level = :debug   # full logging
Selenium::WebDriver.logger.level = :warn   # back to normal
Selenium::WebDriver.logger.output = 'selenium.log'   # log to file