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.1
、webpack-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.log
。 puma
的输出可以被运行 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
考虑一个站点,其中 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.1
、webpack-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.log
。 puma
的输出可以被运行 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