React 和库教程(四)
八、React 部署:MERN 栈
在这一章中,我们将继续上一章中我们停止的地方,并通过将我们的工作应用和 API 发布到实际的远程实时服务器来完成我们的开发周期。本章分为两个主要部分。
-
设置 Ubuntu 服务器
-
使用 Grunt 脚本发布
在我们开始之前,备份您的工作是一个很好的做法。是的,您可以在本地和外部驱动器上备份您的工作,但是在与团队合作时,您通常需要与其他人共享您的工作。一个常见的解决方案是版本控制,Git 是最流行的版本控制解决方案。如果你还没有设置版本控制工具,我强烈建议你在本章中设置它。
为您的项目设置 Git Repo
我们将在本章中使用的命令将把你的代码上传到你为你的项目设置的 Git repo 中,以确保你不会丢失你的工作,并且还会保存你的变更的历史。如果你从来没有设置过 Git repo,注册 GitHub 或者任何你喜欢的版本控制,然后创建一个新的 repo。在 GitHub 上,进入 https://github.com/new 创建一个新的回购。
Note
您可以将回购设置为私有(只有您可以访问)或公共(对全世界开放)。
在项目级别,在终端中设置您的 repo 的用户名和密码。
$ git config --global user.email YOUR-EMAIL@mail.com
$ git config --global user.name "YOUR NAME"
Tip
强烈建议学习和设置 Git,因为它可以帮助确保您永远不会丢失您的工作。如果你不得不在团队中工作并跟踪工作历史,它也会对你有所帮助。它还有其他几个有用的特性。
将 Ubuntu 服务器设置为生产服务器
正如我们在前一章看到的,MERN 栈使开发过程变得更加容易。这不仅适用于开发周期,也适用于发布作品的时间。如果你遵循敏捷和 Scrum 方法论,你就会知道快速发布和经常发布是多么重要。在这一章中,我会告诉你在 Ubuntu 服务器上发布应用的步骤,包括免费设置 SSL。
为什么选择 Ubuntu 服务器?
当涉及到发布你的作品时,有许多服务器可以使用,有许多方法可以选择。您可以设置一个虚拟的非专用服务器,并将前端和后端分成两个独立的服务器,Heroku 和 Windows Server。你甚至可以使用无服务器框架,这里仅举几个例子。选择是无限的。
在这一节中,我们将在 Ubuntu 服务器上发布,但请记住,有许多解决方案,您需要做自己的研究,以找到最适合您正在做的事情和您的确切需求。
在 Ubuntu 服务器上用 MERN 发布有很多好处,因为我们可以设置一台服务器,让它存储我们的后端、前端和数据库。有了这个资源,除了 web 服务器之外,我们还可以运行其他脚本,比如自动化脚本。
此外,Ubuntu 服务器是世界上最流行的用于云环境的 Linux 服务器。Ubuntu 服务器为所有工作负载提供了一个很好的虚拟机(VM)平台,从 web 应用到 NoSQL 数据库,如 MongoDB 和 Hadoop。
我喜欢这种设置,因为如果我想设置 cron 作业和修改头,我可以完全控制一个专用服务器。我以前是把后端和前端分开的;但是,对于中小型非企业项目来说,在同一台服务器上安装两者更为理想。
Ubuntu 16.04,18.04,还是 20.04?
Ubuntu 20.04 LTS,命名为 Focal Fossa,是 Ubuntu 最新的稳定版本。然而,使用最新版本并不总是最好的,因为并不是所有的服务器提供商都有这个选项,而且已经推出两年的服务器可能更“稳定”Ubuntu 版提供了一些不错的功能。它重启速度更快,并有值得探索的新功能,如高级 GUI。将支持到 2025 年 4 月。
Ubuntu 18.04 比 16.04 更稳定,16.04 已经接近生命的尽头,所以一定要麻烦安装 16.04。
如果你用 2018 年 4 月 26 日发布的仿生海狸(Ubuntu 18.04),你将能够获得安全和更新,直到 2023 年 4 月。在我写作的时候,or 20.04 是我对 Ubuntu 服务器的建议。
Ubuntu 19.04 是一个短期支持版本,支持到 2020 年 1 月,所以这个版本应该完全跳过。
如何在 Ubuntu 18.04/20.04 上安装 MERN?
我把这个教程分成了五个步骤。
-
第一步:推出 Ubuntu 服务器。
-
第二步:设置 SSH 并安装服务器软件。
-
第三步:安装 MongoDB。
-
步骤 4:设置身份验证。
-
步骤 5:设置 SSL。
为 MERN 安装 Ubuntu 服务器
在这一节中,我将告诉你为我们的 MERN 代码建立一个 Ubuntu 服务器的步骤。如果您按照我描述的步骤操作,您将拥有一个包含 MongoDB 的 Ubuntu 服务器,它具有身份验证功能以及一个免费的安全套接字层(SSL)证书。
第一步:推出 Ubuntu 服务器
第一步是推出服务器。
亚马逊 EC2 和微软阿祖拉是推出 Ubuntu 服务器最流行的方式。它们都列在 Ubuntu 网站上( https://ubuntu.com/public-cloud )。这些云解决方案可以作为你的服务器使用,但是我推荐你在这里使用 EC2 或者阿祖拉。这些只是建议。每种平台都有其优点和警告,在决定使用什么平台之前,你应该做自己的研究(DYOR)。
EC2 和阿祖拉在 Ubuntu 服务器上提供免费的专用服务器;然而,如果你不密切关注,并超过分配的使用,你经常会发现自己与一个沉重的发票。
亚马逊和阿祖拉都要求你提供你的信用卡信息,他们会在免费周期或使用结束时自动向你收费,无论哪种情况先发生。同样,如果你忘记取消,你将被收费。
Azure 同时提供 Ubuntu Server 20.04 LTS 和 18.04; https://azuremarketplace.microsoft.com/en-us/marketplace/apps/canonical.0001-com-ubuntu-server-focal?tab=Overview见。
在任何专用服务器上,步骤都是相似的。
-
启动 Ubuntu Server 18.04/20.04 LTS 版。
-
设置安全措施。
-
使用 SSH 来限制对服务器的访问,以便只有您的 IP 地址可以登录。
-
下载密钥对。
-
设置公共 IP 地址。
HTTP, HTTPS ports sets as: (0.0.0.0/0, ::/0)
Custom TCP: 27017 (for MongoDB) — (0.0.0.0/0, ::/0)
Port 8000 (for express https server)
Important: For HTTP and HTTPS,
8000
Note
出于安全考虑,我建议将 SSH 设置为您当前的 IP 地址。这比将 SSH 端口暴露给任何 IP 地址要好得多。
这里有一个例子说明如何在 Amazon EC2 免费层上使用 Ubuntu。请遵循以下步骤:
图 8-2
EC2,选择实例类型
图 8-1
EC2,启动 Ubuntu 服务器
-
点击实例,然后选择“Ubuntu Server 18.04 / 20.04 LTS (HVM),SSD 卷类型- ami-06d51e91cea0dac8d (64 位 x86)”,如图 8-1 。
-
实例类型选择 t2.micro,如图 8-2 所示。
-
准备好仓库。您可以免费获得高达 30GB 的空间。
接下来,我们需要配置我们的安全组。
图 8-3
配置安全组
-
配置安全组。使用 SSH、HTTP、HTTPS、自定义 TCP: 27017(对于 MongoDB)、80、8000 和 443(对于 Express HTTPS 服务器)。
重要提示:对于 HTTP 和 HTTPS,选择“8000 (0.0.0.0/0,:/0)”并将 SSH 限制设置为我的 IP,如图 8-3 所示。
-
通过选择 site-api-amazon-key 创建一个新的密钥对,然后单击 Download Key Pair。
-
发射!
Tip
英特尔 x86 几乎普遍比 ARM 快。
在 Azure 上,创建免费帐户后的过程是相似的。先去 https://portal.azure.com/?quickstart=true 。
在快速入门中心,点击“部署虚拟机”,然后点击开始(图 8-4 )。
图 8-4
Azure 快速入门中心
接下来,点击“创建一个 Linux 虚拟机”,如图 8-5 所示。
图 8-5
Azure,部署虚拟机
接下来,使用选项选择 Ubuntu 版本,选择硬盘驱动器,设置安全性,然后启动。
步骤 2:设置 SSH 并安装软件和升级
现在,您已经有了一个运行并准备好的服务器(无论是 EC2、阿祖拉还是任何其他解决方案),我们希望能够 SSH 服务器以及升级服务器的软件。我们将从让我们的生活变得简单开始。我们将创建一个 SSH 快捷方式来访问服务器。在 Mac 上,使用 vim 或您喜欢的编辑器,输入以下内容:
$ vim ~/.ssh/config
Paste the host and the location of the pem file;
Host My-site-name
HostName YOUR-IP-ADDRESS
User ubuntu
IdentityFile location-to-pem/key.pem
Note
你在我们创建的 pem 文件中看到的,代表了你想要的网站名称,选择任何名称。这是您将用于 SSH 服务器的名称。要查找公共实例的 IP 地址,请查看 EC2 实例详细信息页面。
接下来,设置密钥的权限。
$ chmod 600 *.pem
让我们试一试。
$ ssh my-site-name
在 SSH 服务器之前,您需要设置 EC2 来接受 SSH 到您的 IP 地址。
选择 EC2,单击“安全组”,单击“编辑入站规则”,单击 SSH,单击“源类型”,然后单击我的 IP。
完成后,您就可以登录到这个盒子。我喜欢设置一些别名,以便使用预定义的命令来管理服务器。
$ vim ~/.bash_profile
粘贴 PEM 文件的主机和位置。
alias tailall='sudo lnav /home/ubuntu/www/logs'
alias tailwatchers='tail -f /home/ubuntu/www/logs/watch-log.log'
alias cleanlogs='sudo rm -f /home/ubuntu/www/logs/*.* && sudo touch /home/ubuntu/www/logs/server.log && sudo touch /home/ubuntu/www/logs/server-error.log && sudo touch /home/ubuntu/www/logs/watch-error.log && sudo touch /home/ubuntu/www/logs/watch-log.log'
alias l='ls -ltra'
alias c="clear"
alias cls="clear"
alias ll='ls -ltra'
alias killnode='sudo killall -2 node'
export PORT=8081
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8081
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8000
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
export PS1="\u@\h \[\033[32m\]\w\[\033[33m\]\$(parse_git_branch)\[\033[00m\] $ "
这些别名包括 Express 的端口导出、终止所有节点实例的命令、显示 Git 目录的良好设置以及访问工作目录的简洁命令。请记住运行该文件以应用这些更改。
$ . ~/.bash_profile
接下来,我们希望升级并安装我们将需要的软件,比如 Python、NPM 和 Forever(以保持 Node.js 运行)。我们还需要为 Express 和其他软件设置端口。
$ sudo apt-get update
$ sudo apt-get -y upgrade
$ sudo apt-get dist-upgrade
$ sudo apt-get install build-essential
$ sudo apt-get install libssl-dev
$ sudo apt-get install git-core
$ sudo apt-get install python2.7
$ type python3 python2.7
接下来,让我们在 Ubuntu 服务器上安装 Node.js。
$ sudo apt install nodejs
安装 NPM 并更新至最新版本。
$ sudo apt install npm
$ sudo npm install -g npm@latest
现在,创建一个 web 文件夹,我们将在其中放置我们的文件。
$ cd ~
$ mkdir ~/www
$ cd ~/www
全球永久安装( https://github.com/foreversd/forever )。
$ cd ~
$ sudo npm install -g forever
Forever 是一个简单的 CLI 工具,可以确保我们的脚本持续运行(即永远运行)。
接下来,我们要配置默认网站使用端口 8080,而不是端口 80。您可以使用以下命令将端口从 8081 转发到 80:
$ export PORT=8081
$ sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j EDIRECT --to-port 8081
Note
如果您需要删除我们在这里设置的 iptable,只需使用-D标志。
sudo iptables -D 预路由-t nat -i eth0 -p tcp - dport 80 -j 重定向到端口 8081
第 3 步:安装 MongoDB
我们的 MERN 服务器需要一个 MongoDB 服务器。要安装 MongoDB 服务器,请使用以下命令:
安装 MongoDB 服务器。
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4
$ echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
现在,运行更新和安装命令。
$ sudo apt update
$ sudo apt install mongodb-org
$ sudo systemctl unmask mongod
$ sudo systemctl enable mongod
$ sudo service mongod status
您可以将您的 MogoDB 状态输出与我的进行比较,如下所示:
ubuntu@ip-ip ~/www $ sudo service mongod status
mongod.service - MongoDB Database Server
Loaded: loaded (/lib/systemd/system/mongod.service; enabled; vendor preset: enabled)
Active: inactive (dead) since
Docs: https://docs.mongodb.org/manual
Process: 903 ExecStart=/usr/bin/mongod --config /etc/mongod.conf (code=exited, status=
Main PID: 903 (code=exited, status=0/SUCCESS)
systemd[1]: Started MongoDB Database Server.
systemd[1]: Stopping MongoDB Database Server...
systemd[1]: Stopped MongoDB Database Server.
现在我们已经安装了 MongoDB,请更新 MongoDB,以便所有 IP 地址都可以接收来电。为此,编辑mongod.conf文件。
$ sudo vim /etc/mongod.conf
我们需要做的更改是将我们的端口从绑定更改为特定地址。
net:
port: 27017
bindIp: 127.0.0.1
要绑定到所有传入的 IP 地址,请使用:
net:
port: 27017
bindIpAll: true
# bindIp: 127.0.0.1
Note
端口 27017 应该在我们部署服务器时就已经打开了。
现在,要停止/启动/重启 MongoDB 服务以在后台运行,请使用以下命令。要启动它,请使用以下命令:
$ sudo systemctl start mongod
如果您想要停止或重新启动,请使用以下命令:
$ sudo systemctl stop mongod
$ sudo systemctl restart mongod
试一试,确保安装顺利。接下来,让我们创建第一个数据库来尝试 MongoDB。
$ mongo
将您在 Ubuntu 服务器命令行上运行 Mongo 的输出与我的输出进行比较。它可能会输出不同的结果,但会以命令行输入提示:>结束。看一看:
ubuntu@ip $ mongo
MongoDB shell version v4.0.21
connecting to: mongodb://ip:27017/?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("3236f117-2e73-4d78-bd37-eeffef73fb85") }
MongoDB server version: 4.0.20
>
接下来,让我们使用数据库并插入用户。
> use your-database-name
> db.users.insert({"email":"someemail@gmail.com", password:"123456"})
> db.users.find();
以下是预期的输出:
> use your-database-name
switched to db your-database-name
> db.users.insert({"email":"someemail@gmail.com", password:"123456"})
WriteResult({ "nInerted" : 1})
> db.users.find();
{ "_id" : ObjectId("some-objectId"), "email": "someemail@gmail.com", "password": "123456"}
>
蒙戈布客户端
我建议设置一个 MongoDB GUI 客户端,如果您在上一章中还没有这样做的话,这样可以更容易地管理您的数据库。MongoDB Compass 基础版是免费的,易于使用,由 MongoDB 团队制作。 https://www.mongodb.com/products/compass见。
步骤 4:设置身份验证
现在我们已经在远程机器上设置了 MongoDB,下一步是创建身份验证。身份验证不是可选的;它真的是远程服务器上的必备。没有身份验证,任何黑客都可以使用您的 MongoDB 数据库。您不希望未经授权的用户只是连接到您的数据库并在其上放置蠕虫。
若要设置鉴定,请在“终端”中通过 SSH 连接到远程服务器。
$ ssh my-site-name
我们将创建一个新的管理员用户,并为该用户设置身份验证。看一看:
$ mongo admin
$ db.createUser({ user: 'myuser', pwd: '123456', roles: [ { role: 'root', db: 'admin' } ]});
将用户添加到 MongoDB 数据库后的输出应该如下所示:
> db.createUser({ user: 'myuser', pwd: '123456', roles: [ { role: 'root', db: 'admin' } ]});
Successfully added user: {
"user": "myuser",
"roles": [
{
"roles": "root",
"db": "admin"
}
]
}
> db.getUsers()
这将输出用户列表。
[
{
"_id" : "MyDatabase.myuser",
"user" : "myuser",
"db" : "MyDatabase",
"roles" : [
{
"role" : "readWrite",
"db" : "MyDatabase"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
]
退出数据库。
$ exit
Note
在您的密码中,您不能使用@符号作为密码字符串的一部分。当试图从 Express 应用连接到数据库时,您将得到“mongoparserror:un escaped at-sign in authority”。
这里,我在主数据库上设置认证,而不是在我们创建的名为MyDatabase的特定数据库上。
要登录到远程服务器上的数据库,请使用以下命令:
$ mongo -u myuser -p 123456
> use MyDatabase
switched to db MyDatabase
> db.users.find();
{ "_id" : ObjectId("5fb7af6772f31b6884299994"), "email" : "someemail@gmail.com", "password" : "123456" }
对于健全性检查,您可以运行getUsers命令来确保我们的用户被正确添加。
接下来,绑定所有 IP 地址并设置安全性,这样我们就能够在启用安全性的情况下读写数据库,就像我们在本地所做的一样。
$ sudo vim /etc/mongod.conf
net:
port: 27017
bindIpAll: true
bindIp: 0.0.0.0
security:
authorization: 'enabled'
Note
如果你需要的话,你可以启用本地 ip 地址:bindIp: 127.0.0.1。
要应用这些更改,请重新启动。
$ sudo systemctl restart mongod
连接到安全的 MongoDB
现在,我们可以连接到我们的远程服务器,并传递新的认证信息。
$ mongo -u 'myuser' -p '123456'
> use MyDatabase
switched to db MyDatabase
> db.users.find();
{ "_id" : ObjectId("5fb7af6772f31b6884299994"), "email" : "someemail@gmail.com", "password" : "123456" }
或者从我们的本地机器,通过终端,我们可以不使用 SSH 连接。只需告诉 Mongo 要使用的远程 IP 地址、端口和用户。
$ mongo -u 'myuser' -p '123456' remote_ip_address:port/admin
现在,连接 URI 将没有数据库名称,如下所示:
mongodb://myuser:123456@ip_address:27017
删除用户
如果您需要删除用户并设置新用户,或者如果您忘记了密码,您需要首先删除安全性。
$ sudo vim /etc/mongod.conf
然后,用dropUser删除用户(注意removeUser已弃用)。
$ mongo admin
$ db.dropUser(myuser)
你可以在这里找到关于dropUser的信息: https://docs.mongodb.com/manual/reference/method/db.removeUser/#db-removeuser 。
更改存储位置
注意,在mongod.conf文件中,我们可以通过dbPath改变实际存储的位置。现在它被设置在这个位置:
/var/lib/MongoDB
Tip
你可以把这个改成任何你喜欢的位置,比如/data/DB。只要确保您用正确的权限设置了路径。
如果您决定更改数据库的位置,下面是您将如何设置权限,以便 Mongo 可以访问该文件:
$ sudo mkdir -p /data/db
$ sudo chown -R $USER:$USER /data/db
要查看不同路径上的数据库操作,您可以重启 MongoDB 或在新位置运行mongod。
$ mongod --dbpath /data/db
步骤 5:设置 SSL
今天在构建网站时,SSL 不是可选的。这是必须的。如果你的网站没有设置 SSL,浏览器会给你一个错误信息。
你可以为所有域名和服务器注册商设置 SSL,只需支付年费,减少麻烦,并在 SSL 检查中显示一个有信誉的 SSL 证书,但你也可以免费设置。
要免费设置 SSL,可以使用 Certbot ( https://certbot.eff.org/ )。首先,安装软件。
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository universe
$ sudo add-apt-repository ppa:certbot/certbot
Enter to continue
$ sudo apt-get update
For Ubuntu 18.04;
$ sudo apt-get install certbot python-certbot-apache
For Ubuntu 20.04;
$ sudo apt-get install certbot python3-certbot-apache
现在设置证书。
$ sudo certbot certonly --manual
命令行向导会启动并向您提问。
对于站点,使用您的站点名称,如some-site.com。
如果您对记录您的 IP 地址感到满意,请回答 y。请参见 Ubuntu 输出中的“设置 SSL 证书机器人向导”信息。
ubuntu@ip-ip $ sudo certbot certonly --manual Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator manual, Installer None Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): your-email@gmail.com
--------------------------------------------------------------
Please read the Terms of Service at https://letsencrypt.org/documents/LE—SA—v1.2—November-15-2017.pdf. You must agree in order to register with the ACME server at https://acme—v02.api.letsencrypt.org/directory
--------------------------------------------------------------
(A)gree/(C)ancel: A
--------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier Foundation, a founding partner of the Let's Encrypt project and the non—profit organization that develops Certbot? We'd like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom.
--------------------------------------------------------------
(Y)es/(N)o: N Please enter in your domain name(s) (comma and/or space separated) (Enter 'c' to cancel): YourSite.com Obtaining a new certificate Performing the following challenges: http-01 challenge for YourSite.com
接下来,它将要求您证明您有权访问该域。
Create a file containing just this data:
[SOME RANDOM GENERATE CODE]
使其在您的 web 服务器上可用,网址为:
http://some-site.com/.well-known/acme-challenge/KEY
停下来,创建所需的文件。
将目录更改为/home/ubuntu/www/dist/,并按照说明创建文件。
> cd /home/ubuntu/www/dist/
> mkdir .well-known
> cd .well-known
> mkdir acme-challenge
> cd acme-challenge
> vim "[FILE NAME]"
[CODE]
勾选 http://some-site.com/.well-known/acme-challenge/ [string]。
有用吗?太好了。现在我们可以继续了。
Press Enter to Continue
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/MY-SITE-NAME.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/MY-SITE-NAME.com/privkey.pem
设置目录的权限。
$ sudo chown -R ubuntu:ubuntu /etc/letsencrypt
续订证书
要续订证书,请运行以下命令:
sudo certbot certonly --manual -d "your-site.com"
过程是一样的;你只需要准备好挑战。
$ mkdir -p public/.well-known/acme-challenge
接下来,用数据设置文件名。
$ vim /home/ubuntu/www/public/.well-known/acme-challenge/[file name]
[some text]
443 安全
将 SSH 端口添加到安全部分。如果在上一步中没有添加,请使用以下命令:
HTTPS > TCP > 443 > 0.0.0.0/0
Custom > 8000 > 0.0.0.0/0
如果您还记得在vim ~/.bash_profile中,我们正在将端口 443 重定向到 8000。
$ sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8000
当我们创建 HTTPS 服务器时,我们应该指向端口 8000。
let https_server = require('https').createServer({
key: fs.readFileSync('/etc/letsencrypt/live/some-site.com/privkey.pem', 'utf8'),
cert: fs.readFileSync('/etc/letsencrypt/live/some-site.com/cert.pem', 'utf8'),
ca: fs.readFileSync('/etc/letsencrypt/live/some-site.com/chain.pem', 'utf8')
}, app).listen(8000, function () {
logger.info('Listening on https://' + os.hostname() + ':8000');
});
太棒了。
使用 Grunt 脚本发布
当谈到自动化时,如果选择使用某些工具,设置它可能是一个棘手的问题,换句话说,是开发工作流程中一项耗时的任务。MERN 栈使开发过程更加容易。这不仅仅是对发展而言;这也适用于何时发布你的作品。
为什么咕哝?
谈到自动化和部署构建工具,有很多可供选择。我们已经在 CRA 项目中设置了 Webpack 包和 NPM 脚本,那么为什么我们还需要另一个库呢?
咕噜与咕噜、网络包与 NPM 脚本
这个主题确实值得有它自己的一章,但是我将在这里给你一个快速的纲要。当我们查看这些工具的受欢迎程度时,我们会发现 Webpack 是最受欢迎的,Gulp 位居第二。
-
NPM 脚本:33600(
https://github.com/npm/cli) -
咕哝:一万二千(
https://github.com/gruntjs/grunt) -
大口:四万两(
https://github.com/gulpjs/gulp) -
Webpack:5.57 万(
https://github.com/webpack/webpack)
虽然 Webpack 是最受欢迎的,但这并不意味着它是赢家。每个工具都是为解决特定问题而创建的。理解每个工具的用途是很重要的。
让我们后退一步。我们在这里试图实现什么?我们最重要的任务是将我们的文件发布到生产中。我们不需要做测试,林挺格式化,等等。我们在 CRA 已经有了剧本。
Webpack 是一个模块捆绑器。它适用于较大的项目,与 Grunt 和 Gulp 相比,它更难设置。它不是一个任务运行器,所以尽管它有由 Webpack 团队和社区创建的插件,但它真的不是这项工作的最佳工具。
NPM 的剧本怎么样了?CRA 已经提供了运行脚本。事实上,在我们的package.json文件中有一个部分指定了 NPM 运行脚本,包括调用react-scripts来测试和构建部署构建,我们在第二章中看到了这一点。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
当我们构建项目时,这些react-scripts脚本很好地结合到我们的开发服务器中,您可以在这里看到它们:project/node_modules/react-scripts/scripts/start.js。
当我们建立项目时,CRA 就是这样将我们的node_modules文件夹中仅有的 6 个库变成了 1000 多个。我们将通过package.json使用 NPM 运行脚本来自动化任务,但这不是放置自动化任务来帮助运行它们的合适位置。
为您设置的 NPM 运行脚本旨在帮助您自动运行重复性任务,我们将使用它们来设置我们的部署自动化任务。
由于我们希望创建任务来发布我们的代码(而不是构建我们的模块或进行任何林挺、格式化、测试或运行重复的任务),Grunt 和 Gulp 更适合这项工作,它们可以在 NPM 运行脚本中设置。
我们可以排除 Webpack 和 NPM 运行脚本作为将文件发布到远程服务器的最佳工具。参见图 8-6 。
图 8-6
为发布自动化任务寻找合适的工具
Grunt 和 Gulp 是用于自动化任务的,非常适合我们这里需要做的事情。Grunt 是最老的。Grunt 是为任务配置创建的。Gulp 更受欢迎,类似于 Grunt,但是您可以使用它以比设置 Grunt 任务更直观的方式编写 JavaScript 函数。有了 Gulp,你的代码将更具可读性,所以 Gulp 在某些方面优于 Grunt。
但说到插件,Grunt 的插件数量众多,占据上风。
-
咕哝:6250(
https://gruntjs.com/plugins) -
大口:4120(
https://gulpjs.com/plugins/)
就插件而言,我们真的受到开源社区的支配,如果你找不到你需要的插件,你可能需要创建自己的插件,或者如果你的插件失败或没有得到维护,你将不得不自己修复它或添加功能。
此外,为了安全起见,我们需要确保我们的插件做它们应该做的事情,特别是当我们依赖将要进入我们的生产服务器的内容时。这是一个大问题,因为它会将你的整个发布置于危险之中,甚至会使它停止。
例如,在 2018 年末,黑客能够使用 NPM JavaScript 库( https://www.npmjs.com/package/event-stream )成功地将恶意代码插入事件流。这个库被数百万人使用,目标是一家名为 Bitpay 的公司,该公司有一个名为copay ( https://github.com/bitpay/copay )的 Git 库。
像许多开源库一样,开发人员在事件流上的工作没有得到报酬,在将项目交给新的维护者之前,他们对项目失去了兴趣。
新的维护者注入了针对copay的恶意代码。该代码从余额超过 100 比特币或 1000 比特币现金的账户中捕获账户细节和私钥。
copay随后在 5.0.2 版本上更新了其依赖库,并纳入了攻击者代码,导致损失数百万。
该代码捕获受害者的帐户数据和私钥,然后使用服务调用,将数据发送到攻击者的服务器而不被发现。
这次攻击的完整细节和分析可以在这里找到: https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident 。
综上所述,我更喜欢使用 Gulp 而不是 Grunt,因为它更符合 MERN 的 JavaScript 范式。然而,我经常选择 Grunt 而不是 Gulp,因为它的插件数量更多,而且 Grunt 也更成熟。话虽如此,但如果你使用 Gulp,我在这里做的简单任务就不会出错。
如何用 Grunt 发布你的剧本?
我将这一部分的过程分为两步。
-
步骤 1 :将前端发布到远程服务器上(
app/Gruntfile.js) -
步骤 2 :将后端发布到远程服务器上(
api/Gruntfile.js)
使用 Grunt 发布内容
我想发布我们在同一个 Ubuntu 服务器上创建的后端和前端。对于相对较小的中小型应用来说,这是一个很好的设置。在一个更复杂的应用中部署多个服务器、负载平衡器和其他方法,在这里会显得有些矫枉过正,更难维护。
首先,全局安装 Grunt。
$ npm install -g grunt-cli
此外,安装用于运行 shell 命令的grunt-shell插件。
$ yarn add -D grunt-shell
步骤 1:发布前端
您可以从如下所示的 Apress GitHub 位置下载该脚本:
https://github.com/Apress/react-and-libraries/08/app/Gruntfile.js
在 Grunt 中,一切都是为了创建任务。我们将创建两个主要任务。
-
这将是我发布代码的默认任务。
-
local-prod-build:这将是一项模拟生产构建进行测试的任务。
我们的默认任务将执行以下操作:
-
将我们的代码发布到我们为项目设置的 Git repo 中。
-
format:通过 Airbnb 风格指南进行格式化和 lint。你还记得,我们在第一章中设置了格式和林挺。 -
这是构建应用的内置 CRA·NPM 运行脚本。构建 React 应用的生产优化版本。
-
delete_old_files_api:删除旧的dist文件夹,该文件夹用于存放 React 应用的优化版本。 -
copy_new_files_api:将 React 应用的优化版本复制到 API 应用项目内部的 dist 中(这样我们就可以使用 Express 服务器运行我们的应用)。 -
server_upload_app:上传文件到远程 Ubuntu 服务器。 -
stop_node:停止生产服务器上的 Node.js 脚本。这将停止 Express 服务器。 -
start_node:在生产服务器上启动 Node.jsserver.js脚本。这将启动 Express 服务器。
类似地,要设置local-prod-build,该任务需要执行以下任务:
-
将我们的代码发布到我们为项目设置的 Git repo 中。
-
使用 Airbnb 风格指南格式化我们的代码。
-
yarn_build:构建 React 应用的生产优化版本。 -
这是一个 CRA 内置的 NPM 脚本,用来创建一个本地服务器来模拟生产服务器。
-
open_build:打开带有生产版本的本地主机。
为了在代码级别(app/Gruntfile.js)实现这些任务,这些任务和子任务看起来应该是这样的:
// app/Gruntfile.js
module.exports = function (grunt) { grunt.loadNpmTasks('grunt-shell'); grunt.initConfig({ /**
* We read in our `package.json` file so we can access the package name and
* version.
*/
pkg: grunt.file.readJSON('package.json'),shell: {
git_add: {
command: [
'git add .',
'git add -u',
"git commit -m '<%= pkg.version %> -> <%= pkg.commit %>'",
'git push'
].join('&&')
},
lint: {
command: 'yarn run lint'
},
format: {
command: 'yarn run format'
},
yarn_build: {
command: 'yarn build'
},
yarn_serve: {
command: 'serve -s build'
},
open_build: {
command: 'open http://localhost:5000'
},
delete_old_files_api: {
command: 'rm -rf /YOUR-API-LOCATION/app-api/dist'
},
copy_new_files_api: {
command: 'cp -rf /YOUR-APP-LOCATION/app/build/ /YOUR-API-LOCATION/app-api/dist/'
},
server_upload_app: {
command: 'scp -r -i /YOUR-PEM-LOCATION/key.pem' +
' /YOUR-API-LOCATION/app-api/dist/* ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/dist'
},
stop_node: {
command: "ssh -i /YOUR-PEM-LOCATION/key.pem ubuntu@YOUR-UBUNTU-PUBLIC-IP 'sudo pkill -f node'"
},
start_node: {
command: "ssh -i /YOUR-PEM-LOCATION/key.pem ubuntu@YOUR-UBUNTU-PUBLIC-IP 'sudo forever start /home/ubuntu/www/server.js'"
},
}
});
grunt.registerTask('default', ['shell:git_add', 'shell:format', 'shell:yarn_build', 'shell:delete_old_files_api', 'shell:copy_new_files_api', 'shell:server_upload_app', 'shell:stop_node', 'shell:start_node']);
grunt.registerTask('local-prod-build', ['shell:git_add', 'shell:format', 'shell:yarn_build', 'shell:yarn_serve', 'shell:open_build']);
};
注意,对于 Git 任务,我在将用于 Git 注释的package.json文件中设置版本和提交,因此在运行 Grunt 发布脚本之前,需要在package.json中设置版本和提交消息。
{
"name": "api",
"version": "0.0.0872",
"commit": "add service call",
我们现在可以使用一个命令为生产服务器进行部署。
$ grunt
您还可以运行任务,在我们的本地机器上创建生产版本。
$ grunt local-prod-build
我们能让它变得更简单吗?没错。让我们把 Grunt 和 NPM 剧本联系起来。在package.json文件的脚本标签中,添加一个“推送”脚本。
"scripts": {
...
"push": "grunt"
}
既然我们的任务自动化更好地绑定到了我们的 NPM 脚本,我们可以试一试了。
$ yarn push # or npm push
步骤 2:将后端代码发布到远程服务器
接下来,在我们的后端,我们希望能够用一个命令发布 Node.js Express 应用,就像我们对前端代码所做的那样。
您可以从 Apress GitHub 站点下载该脚本。
https://github.com/Apress/react-and-libraries/08/api/Gruntfile.js
我们将创建三个任务。
-
default:默认任务将代码上传到远程服务器。 -
upload-app:上传 React 代码到远程服务器。我们已经在 Grunt 任务中这样做了,但是在这里这样做也没有坏处。 -
node-restart。重启 Node.js,这将重启我们的 Express 应用。
默认任务
我对上传的内容更有选择性,因为我不想每次推送都上传所有文件。请遵循以下步骤:
-
multiple:将 API 代码发布到 Git。 -
server_package:上传package.json文件到远程服务器。 -
stop_node:停止 Node.js。 -
upload_server_file:上传server.js文件。 -
server_upload_services:上传任何 get 类型的服务。 -
server_upload_services_post:上传任何帖子类型的服务。 -
server_upload_utils:上传实用文件夹。 -
start_node:再次启动 Node.js,因为它在上一个任务中已经停止。
你可能需要不同的文件夹和文件,但你知道的。如果需要,可以随意添加。让我们回顾一下这些任务。
上传应用任务
您还记得,使用我们设置的app/Gruntfile.js任务将生产文件从我们的 CRA 复制到 API 服务器的dist文件夹中。我们在这里设置的任务可以使用我们设置的任务将这些文件上传到远程服务器。
节点重启任务
这个任务是在需要的时候重启 Node.js。在不需要 SSH 服务器的情况下,在一个命令中使用它是很好的。我们将在Gruntfile.js中设置两个任务来轻松地停止和启动节点脚本
-
停止节点
-
开始节点
看一下代码,如下所示:
// api/Gruntfile.js
module.exports = function (grunt) { grunt.loadNpmTasks('grunt-replace');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-open'); grunt.initConfig({ /**
* We read in our `package.json` file so we can access the package name and
* version.
*/
pkg: grunt.file.readJSON('package.json'), shell: {
multiple: {
command: [
'git add .',
'git add -u',
"git commit -m '<%= pkg.version %> -> <%= pkg.commit %>'",
'git push'
].join('&&')
},
upload_server_file: {
command: 'scp -r -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/server.js ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/'
},
server_upload_services: {
command: 'scp -r -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/services/* ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/services'
},
server_upload_services_post: {
command: 'scp -r -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/services_post/* ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/services_post'
},
server_package: {
command: 'scp -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/package.json ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/package.json'
},
server_upload_utils: {
command: 'scp -r -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/utils/* ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/utils'
},
server_upload_app: {
command: 'scp -r -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem /YOUR-API-LOCATION/api/dist/* ubuntu@YOUR-UBUNTU-PUBLIC-IP:/home/ubuntu/www/dist'
},
stop_node: {
command: "ssh -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem ubuntu@YOUR-UBUNTU-PUBLIC-IP 'sudo pkill -f node'"
},
start_node: {
command: "ssh -i /YOUR-APP-LOCATION/app/docs/keys/ee-amazon-key.pem ubuntu@YOUR-UBUNTU-PUBLIC-IP 'sudo forever start /home/ubuntu/www/server.js'"
}
}
}); grunt.registerTask('default', ['shell:multiple', 'shell:server_package', 'shell:stop_node', 'shell:upload_server_file', 'shell:server_upload_services', 'shell:server_upload_services_post', 'shell:server_upload_utils', 'shell:start_node']);
grunt.registerTask('upload-app', ['shell:server_upload_app']);
grunt.registerTask('node-restart', ['shell:stop_node','shell:start_node']);
};
太棒了。
我们可以运行默认脚本。
$ grunt
在这一节中,我将这个过程分为两步。
-
步骤 1:用 Grunt 将前端发布到远程服务器
-
步骤 2:用 Grunt 将后端发布到远程服务器
现在我们已经将 React 应用和 API 文件上传到了远程 Ubuntu 服务器上。我们还设置了化名。我们可以使用以下命令。
使用 SSH 连接到服务器。
$ ssh my-site-name
使用cdr别名导航到目录/home/ubuntu/www。
$ cdr
要启动节点而不是$ sudo forever start,我们可以使用startnode别名。
$ startnode
要停止 Node.js,不使用$ sudo forever stop 0,我们可以使用stopnode别名。
$ stopnode
要查看所有别名,请键入以下内容:
$ vimb
如果一切设置正确无误,您应该能够使用您的服务器的公共 IP 地址访问我们在前面章节中构建的应用。
看看 Ubuntu 的一些示例输出,通过 SSH 连接到服务器,停止 Node.js 服务器,并在 Forever 的帮助下再次启动它。然后你可以把你的输出和我的进行比较。
ubuntu@ip ~ $ cdr
ubuntu@ip ~/www $ stopnode # stopall # or stopall
ubuntu@ip ~/www $ startnode
warn: --minUptime not set. Defaulting to: 1000ms
warn: --spinSleepTime not set. Your script will exit if it does not stay up for at least 1000ms
info: Forever processing file: /home/ubuntu/www/server.js
ubuntu@ip ~/www $ shownodes
root 23924 1 0 18:38 ? 00:00:01 /usr/bin/node /home/ubuntu/www/server.js
ubuntu 24052 23003 0 18:41 pts/1 00:00:00 grep node
注意,我使用的是我们在设置服务器时设置的命令,比如shownodes,它显示了所有正在运行的节点(ps -ef | grep node)。如您所知,您可以使用($ vimb)查看命令,这相当于vim ~/.bash_profile。
摘要
在这一章中,我们首先为我们的项目设置了一个 Git repo,这样我们就可以与我们团队的其他成员(如果我们有的话)共享我们的项目,并保存我们的变更历史。我们还将获得使用版本控制的许多其他特性和优势。
我们研究了为什么使用 Ubuntu 服务器以及使用什么版本,并且我们将 Ubuntu 服务器设置为 React 应用和 Node.js Express API 脚本的生产服务器。
接下来,我们在 Ubuntu 服务器上安装了 MERN 栈,并为 MERN 栈设置了 Ubuntu 服务器。一旦我们有了为我们设置的服务器,我们期待发布我们的代码。我们首先看了使用 Grunt 的优势以及我们的其他选择。然后,我们看了如何用 Grunt 发布我们的 React 和 Express 脚本,并创建了一个 Grunt 文件来自动化我们的任务,这样我们就可以经常发布和发布。在下一章,我们将研究如何测试我们的 React 应用。
九、测试第一部分:React 应用的单元测试
在测试 React 应用时,需要考虑三个方面的测试。
-
单元测试:测试能够被隔离的最小的一段代码。
-
集成测试:将单个模块组合在一起进行测试
-
E2E 测试:模拟真实的最终用户体验
在这一章中,你将学习如何像专家一样对 React 应用进行单元测试。
说到单元测试,有很多库可以和 React 一起使用,比如 Jest、Enzyme、Sinon、Mocha、Chai、和 Tape。这些库被认为是最常用的 React 单元测试库。
在这一章中,你将学习如何利用 Jest、Enzyme 和 Sinon 对你的应用进行单元测试。
设置项目
为了了解如何对 React TypeScript 组件进行单元测试,我们将创建一个新项目。我们项目的最终结果将是一个计算器组件。见图 9-1 。
图 9-1
我们的计算器组件的最终结果
首先,我们将使用本书中一直使用的 CRA 项目模板。您还记得,要设置 CRA·MHL 项目模板,我们只需要一行命令。这将设置我们这个项目所需的大部分。
对于项目名称,我们将使用hello-jest-enzyme-ts。
$ yarn create react-app hello-jest-enzyme-ts --template must-have-libraries
一旦安装完成,你会得到一个“快乐的黑客!”消息。接下来,我们将使用generate-react-cli模板 CLI 创建计算器 TS 组件。
$ cd hello-jest-enzyme-ts
$ npx generate-react-cli component Calculator
终端输出将确认这三个文件是为我们创建的:
-
样式表 :
src/components/Calculator/Calculator.scss -
测试 :
src/components/Calculator/Calculator.test.tsx -
组件 :
src/components/Calculator/Calculator.tsx
最后,确保您仍然可以运行项目,并确保一切按预期运行。
$ yarn start
这不是我们第一次讨论单元测试。在前面的章节中,我们运行了format、lint和test命令。
$ yarn format & yarn lint & yarn test
你在第二章中学习了格式化和林挺,并为我们的项目安装了所有的测试库。我们还让generate-react-cli为我们生成测试。我们已经为我们设置了 Jest 测试和package.json run 脚本。
接下来,让我们回顾一下我们将使用的主要测试库。
-
玩笑
-
有房子吗
-
酶
-
不然呢
Jest 库是什么?
Jest ( https://github.com/facebook/jest )是一个脸书 JavaScript 单元测试框架。它可以用于任何 JavaScript 项目。
Jest 是一个测试运行器、断言库和嘲讽库。如果需要,它还会提供快照。以下是它的好处:
-
开发者就绪:这是一个全面的 JavaScript 测试解决方案。它适用于大多数 JavaScript 项目。
-
即时反馈:它有一个快速的、交互式的观看模式,只运行与更改的文件相关的测试文件。
-
快照测试:它捕获大型对象的快照,以简化测试并分析它们如何随时间变化。
你可以在这里找到 Jest 文档: https://jestjs.io/docs/en/tutorial-react 。
什么是笑话王国?
Jest-dom ( https://github.com/testing-library/jest-dom )是一个使用定制匹配器来扩展 Jest 的库,使得对 dom 元素的断言更加容易。
Jest-dom 不需要使用 react 测试库,但是它使得编写我们的测试更加方便。
你可以在这里找到 Jest-dom 文档: https://noriste.github.io/reactjsday-2019-testing-course/book/intro-to-react-testing/jest-dom.html 。
酶是什么?
Enzyme ( https://github.com/enzymejs/enzyme )是一个为 React 构建的 JavaScript 测试实用程序,可以更容易地测试 React 组件的输出。
在给定输出的情况下,您还可以操纵、遍历和以某种方式模拟运行时。
Enzyme 的 API 旨在通过模仿 jQuery 的 DOM 操作和遍历 API 来实现直观和灵活。
Note
Enzyme 可以不用 Jest,但是需要搭配另一个单元测试框架。你可以在这里找到酵素文档: https://airbnb.io/enzyme/docs/api/ 。
不然呢?
Sinon ( https://github.com/sinonjs/sinon )是一个独立的测试 JS 框架,包括测试间谍、存根和模拟。发音是"叹息-非"
你为什么要学笑话和酶?
Jest+Jest-DOM+Enzyme+React = React 项目的完整测试能力。
Jest 和 Enzyme 很好地集成在一起,提供了灵活和创造性的测试能力。Sinon 增加了功能。
以下是一些有趣的事实:
-
Jest 由脸书创建并用于测试其服务和 React 应用。
-
Create-React-App 捆绑了 Jest 和 Jest-DOM;它不需要单独安装。
-
酵素工具是 Airbnb 创造的。
-
Sinon 可以嵌入 Jest + Enzyme 框架,由 Airbnb 赞助。
如果你认为测试既昂贵又耗时,那就试试而不是测试。从长远来看,这将花费你更多的钱。
你知道吗,QA 和软件测试公司 QualiTest 对 1000 多名美国人进行的一项调查显示,88%的人在遇到错误或故障时会放弃一个应用。
-
其中约 51%的人表示,如果他们每天至少遇到一个 bug,他们可能会完全停止使用某个应用。
-
此外,32%的受访者表示,他们可能会在遇到故障时放弃某个应用。
怎样可以学会笑话和酶?
为了帮助你理解 Jest 和 Enzyme,我将本教程中的过程分解为三个步骤。
-
步骤 1 :设置和配置我们的项目
-
步骤 2 :为自定义计算器组件编写代码
-
第三步:测试代码
如果你想更好地理解我们正在使用的库,我们也会深入了解一下。
创建和测试计算器组件
在本节中,我们将创建自定义的 React 组件,这是一个计算器,并设置一些测试。我们的组件将被打印,我们将利用 Jest 和 Enzyme 来测试组件。
步骤 1:设置和配置我们的项目
对于 Enzyme,我们希望安装 React 16 适配器(这是最新版本,但在您阅读本文时可能会发生变化),它可以与 React 17 一起工作。我们还需要安装react-test-renderer,这样我们就可以将 React 组件呈现为纯 JavaScript 对象,而不依赖于 DOM。这将有助于我们拍摄快照。
Note
酶、酶适配器和 JSON 库的酶都是 CRA·MHL 模板项目自带的,所以不需要安装它们。不过,如果您想从 CRA 的普通版本开始,下面是命令:
$ yarn add enzyme enzyme-adapter-react-16 react-test-renderer
我们将使用 Jest 的快照测试特性来自动将 JSON 树的副本保存到一个文件中,并通过我们的测试来检查它是否没有改变。
在这里阅读更多关于这个特性: https://reactjs.org/docs/test-renderer.html 。
我们想让我们的生活更简单,所以我们将安装enzyme-to-json库( https://github.com/adriantoine/enzyme-to-json#readme )来简化我们的代码。查看本章中的“引擎盖下”一节,了解更多详情。
$ yarn add -D enzyme-to-json
对于我们的计算器,我将使用 macOS 内置计算器来计算我的图形。我将使用图像映射来映射每个键,把图像变成一个可点击的按钮。
为了做到这一点,我们将安装一个react-image-mapper组件( https://github.com/coldiary/react-image-mapper ),它让我们映射图像的区域。
$ yarn add react-image-mapper
我们已经建立了项目,并安装了开始项目所需的所有库。
步骤 2:创建我们的定制组件
在代码级别,我正在创建一个定制的计算器组件。我所做的是截取一张 macOS 计算器的截图,然后使用一个在线工具将该图像(image-map.net)映射到一个可点击的地图区域。我没有将图形用于生产部署。我在这里只是用它来说明测试过程。如果您构建生产代码,您将需要获得使用该图形的权限。此外,请注意我使用的是image-map.net,但是有许多在线工具和程序来绘制图像,我与该网站没有任何关系。
您可以从 Apress GitHub 下载整个项目。
https://github.com/Apress/react-and-libraries/09/hello-jest-enzyme-ts
我们一起来复习一下代码。
// src/components/Calculator/Calculator.tsx
import React from 'react'
import './Calculator.scss'
注意,对于ImageMapper组件,我在import语句前放置了一个@ts-ignore注释。那是因为ImageMapper组件是 JS 组件,不是为 TS 设置的,没有类型。这将导致 Lint 编译时错误。
// @ts-ignore
import ImageMapper from 'react-image-mapper'
接下来,我将设置放置图像的 URL,并绘制图像区域。我正在使用export常量语句,所以如果需要的话,我可以在我的测试文件中使用相同的const MAP代码。
export const URL = 'calculator.jpg'
export const MAP = {
name: 'my-map',
areas: [
{
name: '0',
shape: 'rect',
coords: [3, 387, 227, 474],
},
{
name: '1',
shape: 'rect',
coords: [2, 291, 112, 382],
},
{
name: '2',
shape: 'rect',
coords: [116, 290, 227, 382],
},
{
name: '3',
shape: 'rect',
coords: [342, 382, 232, 290],
},
{
name: '4',
shape: 'rect',
coords: [3, 194, 111, 290],
},
{
name: '5',
shape: 'rect',
coords: [115, 193, 227, 290],
},
{
name: '6',
shape: 'rect',
coords: [231, 194, 343, 290],
},
{
name: '7',
shape: 'rect',
coords: [4, 97, 111, 191],
},
{
name: '8',
shape: 'rect',
coords: [115, 99, 227, 191],
},
{
name: '9',
shape: 'rect',
coords: [231, 98, 343, 191],
},
{
name: '+',
shape: 'rect',
coords: [348, 291, 463, 382],
},
{
name: '-',
shape: 'rect',
coords: [348, 195, 463, 290],
},
{
name: '*',
shape: 'rect',
coords: [348, 98, 463, 191],
},
{
name: '/',
shape: 'rect',
coords: [348, 3, 463, 93],
},
{
name: '=',
shape: 'rect',
coords: [348, 387, 463, 474],
},
],
}
接下来,我用prop接口(ICalculatorProps)和状态接口(ICalculatorState)设置类定义语句。我也在为类本身设置一个接口(ICalculator)。这样做的原因是我可以转换我的类的一个实例,我将在我的测试中使用这个接口。
对于ICalculator,它将具有与 React 组件语句相同的签名。它需要包含我们将要定义和使用的类的方法:startOver、calculateTwoNumbers和clicked。对于startOver,我将它设置为?只是为了向你展示如何设置一个不需要实现的方法。
export interface ICalculator extends React.PureComponent<ICalculatorProps, ICalculatorState> {
startOver?(): void
calculateTwoNumbers(num1: number, num2: number, operator: string): void
clicked(btnName: string): void
}
对于ICalculatorProps,我将传递一个标题和版本。
interface ICalculatorProps {
componentTitle: string
version: string
}
对于ICalculatorState,我们需要存储计算器的输出,即运算符类型和我们正在处理的两个数字。
interface ICalculatorState {
output: number
operatorType: string
number1: number
number2: number
}
现在我们已经准备好了接口,我们可以定义我们的Calculator类了。
export default class Calculator
extends React.PureComponent<ICalculatorProps, ICalculatorState>
implements ICalculator {
constructor(props: ICalculatorProps) {
super(props)
对于初始状态,我们设置默认值。
this.state = {
output: 0,
operatorType: '',
number1: 0,
number2: -1,
}
}
startOver方法只是将初始默认值设置回计算器。我们可以用它来重新开始,计算新的数字。
startOver = () => {
this.setState((prevState) => {
return {
...prevState,
operatorType: '',
number1: 0,
number2: -1,
output: 0,
}
})
}
我们举重若轻的方法是calculateTwoNumbers。它需要两个数字和一个运算符,然后进行数学运算。
calculateTwoNumbers = (num1: number, num2: number, operator: string) => {
let retVal = 0
switch (operator) {
case '+':
retVal = num1 + num2
break
case '-':
retVal = num1 - num2
break
case '*':
retVal = num1 * num2
break
case '/':
retVal = num1 / num2
break
default:
// eslint-disable-next-line no-alert
alert('Operator not recognized')
}
return retVal
}
点击处理程序期望按钮名称被传递,一个开关将处理不同的用例。我只实现了数字、加号和等号按钮,但是请继续并完成这段代码。
clicked = (btnName: string) => {
switch (btnName) {
case '-':
case '*':
case '/':
case '+':
this.setState((prevState) => {
const newState = Number(prevState.output)
return {
...prevState,
operatorType: btnName,
number1: newState,
}
})
break
case '=':
this.setState((prevState) => {
const newState = this.calculateTwoNumbers(
prevState.number1,
Number(prevState.output),
prevState.operatorType
)
return {
...prevState,
output: newState,
}
})
break
default:
this.setState((prevState) => {
const isFirstDigitNumber2 = prevState.operatorType && prevState.number2 === -1
const newNumberState = isFirstDigitNumber2 ? 0 : prevState.number2
const newOutput = isFirstDigitNumber2
? Number(btnName)
: Number(prevState.output + btnName)
return {
...prevState,
number2: newNumberState,
output: newOutput,
}
})
}
}
对于 render 方法,我们将创建一个按钮来单击重新开始,并将其链接到startOver方法。对于计算器,我们将使用设置了映射区域MAP的ImageMapper组件。
render() {
return (
<>
<a href="http://twitter.com/elieladelrom" className="follow">
@elieladelrom
</a>
<h1 className="title">
{this.props.componentTitle} - Version #{this.props.version}
</h1>
<p>
<button id="btn" onClick={this.startOver}>Start Over</button>
</p>
<div className="calculator-output">{this.state.output}</div>
<ImageMapper src={URL} map={MAP} onClick={(area: { name: string }) => { this.clicked(area.name) } } />
</>
)
}
}
注意,我已经用我的 Twitter 句柄添加了一个div。这可能看起来像是无耻的自我推销,但我想把它包括进来,以便向您展示如何做一些测试。
这与允许我们重新开始的按钮和我们将设置为属性的标题是一样的。我们将使用这两者进行测试。
对于 SCSS,我正在创建一个样式来格式化我的输出。
// src/components/Calculator.scss
.calculator-output
{
background:url('/calculator-input.jpg') no-repeat right top;
color:#fff;
font-size:50px;
width: 464px;
height: 120px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
接下来,我们需要更新我们的App.js文件以包含我们的定制组件,这样当我们转到http://localhost:3000时就可以看到我们的组件。
// src/App.ts
import React from 'react';
import './App.scss';
import Calculator from "./components/Calculator";
function App() {
return (
<div className="App">
<Calculator componentTitle="Online Calculator" version="0.01-beta"/>
</div>
);
}
export default App
;
就这样。我们现在可以运行代码($ yarn start),并且能够查看和使用我们的计算器。
正如我之前提到的,我没有映射所有的计算器键和功能,只是基本的,但可以随意完成任务。最终结果见图 9-1 。
步骤 3:测试代码
在我们运行测试之前,我们需要对我们的测试环境做更多的配置。我还想向您展示开箱即用的产品。好消息是,我们需要的大部分已经为您设置了 CRA MHL 模板项目。
设置适配器
打开src/setupTests.ts,你会看到它配置了酶适配器和 Jest-dom。这允许我们添加自定义 Jest 匹配器来断言 DOM 节点。
// src/setupTests.ts
import '@testing-library/jest-dom/extend-expect'
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() });
使用 enzyme-to-json 设置快照
为了获得快照,我们需要在package.json文件中创建snapshotSerializers标签。这将创建一个快照并加速 Jest 测试。
// package.json
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
},
一旦我们的测试发生变化,快照将需要更新(按 U 键更新),否则您将收到一条错误消息。参见图 9-2 。
图 9-2
测试套件中的快照失败
这在我们运行 Jest 并持续使用 Jest watcher 特性的--watch标志时尤其有用。
Tip
Jest 中的--watch标志将持续运行您的测试。
你不需要设置任何东西。package.json文件已经包含了一个 NPM 运行脚本,它使用react-scripts来运行带有观察器的测试。
// package.json
"scripts": {
...
"test": "react-scripts test"
}
对于我们的测试,我们有两个现成的测试,CRA·MHL 负责App.test.tsx和AppRouter.test.tsx。事实上,在前面的章节中,我们运行的是$yarn测试命令,这些测试是为我们运行的。
测试应用组件:App.test.tsx
打开通过 CRA 模板项目提供的src/App.test.js文件。我们在这里的测试是为了确保我们的计算器组件包含在我们的App.tsx组件中,并且不会崩溃。
// src/App.test.tsx
describe('<App />', () => {
let component
beforeEach(() => {
component = shallow(<App />)
})
test('It should mount', () => {
expect(component.length).toBe(1)
})
})
测试路由组件:AppRouter.test.tsx
AppRouter.test.tsx也是如此。我们希望确保添加路由时不会崩溃。我们还可以进行其他测试。这只是基本的测试,理想情况下,我们希望涵盖每个功能。
// src/AppRouter.test.tsx
import React from 'react'
import { shallow } from 'enzyme'
import AppRouter from './AppRouter'
describe('<AppRouter />', () => {
let component
beforeEach(() => {
component = shallow(<AppRouter />)
})
test('renders without crashing', () => {
expect(component.length).toBe(1)
})
})
现在我们已经准备好了第一个测试,我们可以运行它了。
为了在我们的模板上运行我们的测试,我们的项目已经配备了一个测试脚本来处理一切。我们可以使用 CRA 提供的测试 NPM 脚本。运行测试命令。
$ yarn test # or npm run test
我们第一次使用测试脚本时,我们得到一条消息,如下所示:
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press u to update failing snapshots.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
现在的情况是创建了一个快照,我们可以按 A 来运行所有的测试。这个脚本将继续运行,并不断更新,因此您可以确保在编写代码时没有破坏任何功能。
我们获得了完整的测试套件、测试和测试的快照结果。见图 9-3 。
图 9-3
测试套件、测试和快照测试结果
我们只创建了一个测试。然而,当我们运行generate-react-cli命令时,我在templates/test.js中为您包含的模板文件为我们创建了测试,这是一个典型的测试模板。让我们看一下代码。
// src/components/Calculator/Calculator.test.tsx
import React from 'react';
import { shallow } from 'enzyme';
import Calculator from './Calculator';
describe('<Calculator />', () => {
let component;
beforeEach(() => {
component = shallow(<Calculator />);
});
test('It should mount', () => {
expect(component.length).toBe(1);
});
});
测试是检查计算器组件是否已安装并且没有出现故障。我希望您注意到,在我们的计算器组件文件夹(src/components/Calculator)中,添加了一个名为__snapshots__的新文件夹。见图 9-4 。
图 9-4
计算器组件内的快照文件夹
这个文件夹保存了测试文件Calculator.test.tsx.snap,只要我们的 Yarn 测试脚本还在运行,它就会被更新。要停止脚本,只需按 Command+C。
当我们更改代码时,测试观察特性会告诉我们测试中断了。例如,要破解代码,清空Calculator.test.tsx文件的内容并保存它。见图 9-5 。正如您所看到的,由于测试文件中至少缺少一个测试,我们的测试中断了。
图 9-5
由于清除 Calculator.tsx 文件中的代码,测试套件失败
创建我们的计算器组件测试文件
Jest 的工作方式是,它将在以下任何位置查找测试文件:
-
__tests__文件夹中带有.js和.ts后缀的文件 -
带有
.test.js和.test.ts后缀的文件 -
带有
.spec.js和.spec.ts后缀的文件
generate-react-cli自动为我们创建Calculator.test.tsx。我喜欢测试后缀,因为它很简单,但是.spec.js是其他框架的标准,比如 Angular,所以无论你选择什么都可以。
让我们重构我们的代码。在我们的组件测试文件中,我们可以测试一些东西。首先导入 React,我们将使用的酶 API(shallow和mount,以及我们的自定义组件计算器。
// src/components/Calculator/Calculator.test.tsx
import React from 'react'
import { shallow } from 'enzyme'
import Calculator, { ICalculator } from './Calculator'
我们需要 React 库,Enzyme 项目中的shallow和mount特性来访问 DOM 上的方法,最后是我们创建的ICalculator接口,这样我们就可以对我们的对象进行造型。
非交互组件的单一测试
为了创建一个非交互式组件的单一测试,我们可以测试我的无耻的自我推销 Twitter 帐户。
我们可以为我们的定制组件创建一个包装器,并且可以使用带有find方法的shallow特性来访问我们的链接 DOM 标签中的文本字段。
// non-interactive components - using it (single test)
it('should render the link url', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const a = wrapper.find('a')
const result = a.text()
expect(result).toBe('@elieladelrom')
})
Note
要创建一个单独的测试,您可以使用test关键字或者it关键字。他们是一样的,只是别名。
快照测试套件
到目前为止,我们已经创建了一个测试。但是,如果我们想将几个测试组合在一起呢?为此,我们将使用一个测试套件。一个测试套件创建了一个将几个测试组合在一起的模块。我们可以列出所有我们想包含的测试。
要创建一个测试套件,我们可以使用以下格式将这些单个测试组合在一起:
describe('`Calculator Snapshots', () => {
it ...
it ..
})
例如,要确保我们的包装器与快照匹配,并将测试包装为测试套件,请使用:
describe('`Calculator Snapshots', () => {
it('should render our Snapshots correctly', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
expect(wrapper).toMatchSnapshot()
})
})
测试组件属性
例如,使用测试套件设置一个测试来检查我们从主入口点传递过来的标题上的props,这个过程类似于我们之前的测试。我们设置一个包装器,并根据我们注入的数据检查h1文本标签。
// it(is aliased by test so it does the same thing as it)
test('should render component title', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const title = wrapper.find('h1.title').text()
expect(title).toBe('Online `Calculator - Version #0.01-beta')
})
测试交互式按钮
对于交互式组件,我们可以使用simulate方法来模拟用户点击按钮的手势。然后我们可以比较输出中的结果。我们正在重新开始,所以输出应该会清除。
test('Testing output indirectly - should clean our result box clicking clear', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const btn = wrapper.find('#btn')
btn.simulate('click')
const output: string = wrapper.find('.calculator-output').text()
expect(output).toBe('0')
})
测试套件以直接测试功能
有时候我们需要直接测试函数。看一看。我在这里创建一个测试套件来测试我的calculateTwoNumbers。在测试套件中,我可以包含对所有不同操作符的测试。
describe('Testing `Calculator calculateTwoNumbers testsuite directly', () => {
test('Testing calculateTwoNumbers Directly - add', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const instance = wrapper.instance() as ICalculator
expect(instance.calculateTwoNumbers(1, 2, '+')).toBe(3)
})
test('Testing calculateTwoNumbers Directly - multiple', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const instance = wrapper.instance() as ICalculator
expect(instance.calculateTwoNumbers(2, 2, '*')).toBe(4)
})
})
注意,包装器上的instance()属性非常强大,因为我们可以访问我们的方法。
因为我们使用的是 TS 而不是 JS,所以我们可以转换我们创建的ICalculator接口来访问我们的方法(静态类型)并确保我们的类型是正确的。这个东西很重要,因为它真的让 TS 比普通的 JS 更耀眼。
const instance = wrapper.instance() as ICalculator
instance.calculateTwoNumbers(...)
测试交互按钮和我们的状态
使用instance(),我们也可以测试我们的状态。例如,这里有一个测试来检查如果我们单击 1 按钮,输出会产生 1:
test('test clicked calculator button method', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const instance = wrapper.instance() as ICalculator
instance.clicked('1')
expect(wrapper.state('output')).toBe(1)
})
使用间谍进行测试
一旦我们的应用增长,我们可能需要与异步数据交互,Jest 有内置的功能来处理模拟数据和函数。
再看更高级的测试题目比如间谍和 mock:https://jestjs.io/docs/en/jest-object。
test('spy', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const instance = wrapper.instance() as ICalculator
jest.spyOn(instance, 'startOver')
wrapper.find('button').simulate('click')
expect(wrapper.state('output')).toBe(0)
})
一旦我们所有的测试套件和存根都准备好了,您应该会看到如图 9-6 所示的测试输出。
图 9-6
包括所有计算器测试的快照摘要
在后台
这里有很多事情正在发生。我想指出一些可以帮助你的事情。
酶包括三种渲染方法。我们只用了Shallow,但是还有Mount和Render。这些方法提供了对 DOM 树的访问。
增加
这些是Mounting的优势:
-
包括子组件的完整 DOM 呈现
-
非常适合需要与 DOM API 交互的组件或者必须使用 React 生命周期方法的用例
-
允许访问直接传入根组件的
props(包括默认的props)和传入子组件的props
浅的
这些是Shallow的优势:
-
仅渲染单个组件,不渲染子组件。这对于隔离纯单元测试的组件很有用。它防止子组件中的更改或错误。
-
默认情况下,浅层组件可以访问生命周期方法。
-
它不能访问传入根组件的
props,但是它可以访问传入子组件的 props,并且可以测试传入根组件的props的效果。 -
当我们调用
shallow(<Calculator />)时,我们测试的是计算器呈现的内容,而不是我们传递给Shallow的元素。
提出
这些是渲染的优点:
-
它呈现静态 HTML,包括子元素。
-
它无权使用 React 生命周期方法。
-
与其他 API 相比,它使用的资源较少,但功能较少。
酶转 json
在我们的代码中,我们使用了enzyme-to-json,但是为什么呢?
这样做是为了我们的快照比较有一个比酶的开箱即用的内部组件表示更好的组件格式。
在使用快照时,snapshotSerializers API 允许我们编写更少的代码并消除重复。在将组件传递给 Jest 的快照匹配器之前,我们不需要每次用a.toJson()创建组件时都进行序列化。
expect(toJson(rawRenderedComponent)).toMatchSnapshot();
我们通过在package.json中添加snapshotSerializers来做到这一点。
"snapshotSerializers": ["enzyme-to-json/serializer"],
然后,我们能够将一个由 Enzyme 创建的组件传递给 Jest .toMatchSnapshot()方法,而不使用toJson语法。
expect(wrapper).toMatchSnapshot()
有火柴吗
当我们编写测试时,我们需要检查我们的测试值是否满足某些条件。Expect API 让我们可以访问许多“匹配器”,帮助我们验证不同的东西。
Jest 方面,Expect API 为大多数用户提供了足够的选项;https://jestjs.io/docs/en/expect见。但是,如果您找不到您想要的,请查看 Jest 社区的jest-extended以获得更多匹配,或者如果您找不到您需要的,请创建您自己的匹配。
否则图书馆
另一个我们应该知道并添加到我们工具箱中的必备库是 Sinon ( https://github.com/sinonjs/sinon )。您不需要添加它,因为它已经包含在我们的 CRA 模板项目中。(如果您确实需要添加它,您可以使用yarn add sinon @types/sinon。)
玩笑和兴农的目的是一样的。那么为什么要加西农呢?
答案是,有时您可能会发现一个框架比另一个框架更自然、更容易用于您需要的特定测试,所以两个都有也无妨。可以对比一下 Jest ( https://jestjs.io/docs/en/api )和 Sinon ( https://sinonjs.org/releases/v9.2.0/ )上的 API 列表。
举个例子,伪造计时器。这两个框架都有伪造时间的方法。
幸运的是,我们可以添加 Sinon 并使用 Jest 或 Sinon 进行测试。他们一起工作,所以我们不需要选择。
假设我们想要为我们的计算器创建一个加载器,因为我们需要一个服务调用或者等待我们需要的任何东西。
这次我们可以从测试开始,而不是先写代码。为此,我们可以使用测试驱动开发(TDD ),首先编写测试,然后让测试失败。最后,我们编写通过测试的代码。
TDD 是一个软件开发过程,它依赖于一个开发周期,这个周期需要建立测试用例,然后编写代码以便通过测试。
例如,假设我们的需求是,我们需要放置一个带有消息的副标题,告诉用户加载阶段已经完成。
使用假 Sinon 计时器的测试看起来会是这样的:
import sinon from 'sinon'
describe('Loader component', () => {
it('should render complete after x seconds', () => {
const wrapper = shallow(<Calculator componentTitle="Online `Calculator" version="0.01-beta" />)
const clock = sinon.useFakeTimers()
const instance = wrapper.instance() as ICalculator
instance.startLoader(3000)
clock.tick(3000)
const title = wrapper.find('h1.subTitle').text()
expect(title).toBe('Loading Complete')
})
})
我们这里的代码使用了sinon.useFakeTimers,一旦我们调用了我们的类startLoader上的方法,我们就可以启动时钟:clock.tick(3000)。最后,我们确保副标题更改为“加载完成”
现在,在我们的Calculator.tsx代码中,让我们重构ICalculator接口和ICalculatorState状态的签名。
export interface ICalculator extends React.PureComponent <ICalculatorProps, ICalculatorState> {
...
startLoader(seconds: number): void
}
interface ICalculatorState {
...
LoaderStatus: string
}
接下来,我们可以让一个计时器在组件挂载后开始运行三秒钟,并设置一个startLoader方法来启动计时器并更新状态。
componentDidUpdate() {
this.startLoader(3000)
}
startLoader = (seconds: number) => {
setTimeout(() => {
this.setState((prevState) => {
const newState = 'Loading Complete'
return {
...prevState,
LoaderStatus: newState,
}
})
}, seconds)
}
最后,在渲染 JSX 端,添加一个带有字幕的h1标签。
render() {
return (
<>
...
<h1 className="subTitle">
{this.state.LoaderStatus}
</h1>
...
</>
)
}
}
运行测试以确保一切按预期运行。
测试路由页面
在我们结束本章之前,我想指出 CRA 模板项目附带的另一个测试文件。
如果您还记得,在之前的练习中,我们使用路由构建了我们的应用。我们的路由页面正在扩展RouteComponentProps。这很有用,因为我们能够使用路由 API 并提取路由页面的名称。例如,对于文章页面,我们设置了这个接口签名,它是通过generate-react-cli为我们创建的模板页面。
interface IArticlesPageProps extends RouteComponentProps<{ name: string }> {
// TODO
}
此接口使用路由历史 API 提取页面名称;然后我们就可以在渲染中使用这个名字了。
generate-react-cli还为我们创建了一个名为ArticlesPage.test.tsx的配套文件,这是一个 Jest 测试页面,包括一个通过routeComponentPropsMock访问路由的方法,这是我创建的一个模拟对象。
注意,routeComponentPropsMock只实现了RouteComponentProps的历史 API,所以如果你使用其他 API,你需要在routeComponentPropsMock对象中模仿它们。看一看:
// ArticlesPage.test.tsx
import React from 'react'
import { shallow } from 'enzyme'
import ArticlesPage from './ArticlesPage'
const routeComponentPropsMock = {
history: {
location: {
pathname: '/ArticlesPage',
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
location: {} as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
match: {} as any,
}
describe('<ArticlesPage />', () => {
let component
beforeEach(() => {
component = shallow(<ArticlesPage {...routeComponentPropsMock} />)
})
test('It should mount', () => {
expect(component.length).toBe(1)
})
})
我们可以确认应用正在通过format、lint和test命令。
$ yarn format & yarn lint & yarn test
我想指出的是,lint和format命令也会检查测试文件,所以我们使用 Airbnb 样式设置的林挺规则仍然适用。
摘要
在这一章中,我们关注单元测试。我将这个过程分为三个步骤:设置和配置项目、编写代码和测试代码。
你学习了 Jest、Jest-dom、Enzyme 和enzyme-to-json。您学习了如何在 React 组件上执行不同类型的单元测试,更好地理解相关的库,创建快照,创建单个测试,以及在测试套件中对测试进行分组。
我们查看了我为您设置的模板的单元测试组件和 page 的测试模板,以及App.test.tsx和AppRouter.test.tsx测试。
我也给了你一些额外的“引擎盖下”的东西,最后,我们考虑添加 Sinon 库和 TDD。
在下一章,我们将深入集成测试。