测试驱动开发 测试前移

I’ve been programming for five years and, honestly, I have avoided test-driven development. I haven’t avoided it because I didn’t think it was important. In fact, it seemed very important–but rather because I was too comfortable not doing it. That’s changed.

我已经编程五年了,说实话,我避免了测试驱动的开发。 我没有回避它,因为我认为这并不重要。 实际上,这似乎非常重要-而是因为我很不愿意这样做。 改变了。

什么是测试? (What is Testing?)

Testing is the process of ensuring a program receives the correct input and generates the correct output and intended side-effects. We define these correct inputs, outputs, and side-effects with specifications. You may have seen testing files with the naming convention filename.spec.js. The spec stands for specification. It is the file where we specify or assert what our code should do and then test it to verify it does it.

测试是确保程序接收正确的输入并生成正确的输出和预期的副作用的过程。 我们用规范定义这些正确的输入,输出和副作用。 您可能已经看到了使用命名约定filename.spec.js测试文件。 spec代表规格。 在该文件中,我们指定或声明代码应执行的操作,然后对其进行测试以验证其作用。

You have two choices when it comes to testing: manual testing and automated testing.

关于测试,您有两种选择:手动测试和自动测试。

手动测试 (Manual Testing)

Manual testing is the process of checking your application or code from the user’s perspective. Opening up the browser or program and navigating around in an attempt to test functionality and find bugs.

手动测试是从用户角度检查应用程序或代码的过程。 打开浏览器或程序并四处导航,以尝试测试功能并查找错误。

自动化测试 (Automated Testing)

Automated testing, on the other hand, is writing code that checks to see if other code works. Contrary to manual testing, the specifications remain constant from test to test. The biggest advantage is being able to test many things much faster.

另一方面,自动化测试正在编写代码,以检查其他代码是否有效。 与手动测试相反,各个测试的规格保持不变。 最大的优势是能够更快地测试许多东西。

It’s the combination of these two testing techniques that will flush out as many bugs and unintended side-effects as possible, and ensure your program does what you say it will. The focus of this article is on automated testing, and in particular, unit testing.

正是这两种测试技术的结合,将消除尽可能多的错误和意外的副作用,并确保您的程序按照您的意愿行事。 本文的重点是自动化测试,尤其是单元测试。

There are two main types of automated tests: Unit and End-to-End (E2E). E2E tests test an application as a whole. Unit tests test the smallest pieces of code, or units. What is a unit? Well, we define what a unit is, but in general, it’s a relatively small piece of application functionality.

自动化测试主要有两种类型:单元测试和端到端(E2E)。 端到端测试对整个应用程序进行测试。 单元测试测试最小的代码段或单元。 什么是单位? 好了,我们定义了一个单元是什么,但是一般来说,它只是一个相对较小的应用程序功能。

回顾: (Recap:)

  1. Testing is verifying our application does what it should.测试正在验证我们的应用程序应执行的操作。
  2. There are two types of tests: manual and automated有两种类型的测试:手动和自动
  3. Tests assert that your program will behave a certain way. Then the test itself proves or disproves that assertion.

    测试断言您的程序将以某种方式运行。 然后,测试本身将证明或反对该断言。

测试驱动开发 (Test-Driven Development)

Test-driven development is the act of first deciding what you want your program to do (the specifications), formulating a failing test, then writing the code to make that test pass. It is most often associated with automated testing. Although you could apply the principals to manual testing as well.

测试驱动的开发是首先确定您希望程序执行的操作(规范),制定失败的测试, 然后编写代码以使测试通过。 它通常与自动化测试关联。 尽管您也可以将原理应用于手动测试。

Let’s look at a simple example: Building a wooden table. Traditionally, we would make a table, then once the table is made, test it to make sure it does, well, what a table should do. TDD, on the other hand, would have us first define what the table should do. Then when it isn’t doing those things, add the minimum amount of “table” to make each unit work.

让我们看一个简单的例子:建立一张木桌。 传统上,我们将创建一个表,然后在创建表后对其进行测试,以确保它可以正常工作。 另一方面,TDD将让我们首先定义表应执行的操作。 然后,当它不执行这些操作时,添加最小数量的“表”以使每个单元正常工作。

Here an example of TDD for building a wooden table:

以下是用于构建木桌的TDD示例:

I expect the table to be four feet in diameter.The test fails because I have no table.I cut a circular piece of wood four feet in diameter.The test passes.__________I expect the table to be three feet high.The test fails because it is sitting on the ground.I add one leg in the middle of the table.The test passes.__________I expect the table to hold a 20-pound object.The test fails because when I place the object on the edge, it makes the table fall over since there is only one leg in the middle.I move the one leg to the outer edge of the table and add two more legs to create a tripod structure.The test passes.

This would continue on and on until the table is complete.

这将持续进行直到表格完成。

回顾 (Recap)

  1. With TDD, test logic precedes application logic.对于TDD,测试逻辑先于应用程序逻辑。

一个实际的例子 (A Practical Example)

Imagine we have a program that manages users and their blog posts. We need a way to keep track of the posts a user writes in our database with more precision. Right now, the user is an object with a name and email property:

想象一下,我们有一个程序可以管理用户及其博客文章。 我们需要一种方法来更精确地跟踪用户在数据库中撰写的帖子。 现在,用户是一个具有名称和电子邮件属性的对象:

user = { name: 'John Smith', email: 'js@somePretendEmail.com'
}

We will track the posts a user creates in the same user object.

我们将跟踪用户在同一用户对象中创建的帖子。

user = { name: 'John Smith', email: 'js@someFakeEmailServer.com'posts: [Array Of Posts] // <-----
}

Each post has a title and content. Instead of storing the entire post with each user, we’d like to store something unique that could be used to reference the post. We first thought we would store the title. But, if the user ever changes the title, or if–although somewhat unlikely–two titles are exactly the same, we’d have some issues referencing that blog post. Instead, we will create a unique ID for each blog post that we will store in the userObject.

每个帖子都有标题和内容。 与其与每个用户一起存储整个帖子,我们不希望存储可以用来引用该帖子的独特内容。 我们首先想到会存储标题。 但是,如果用户曾经更改标题,或者(尽管不太可能)两个标题完全相同,那么引用该博客帖子就会遇到一些问题。 相反,我们将为每个将存储在user Object中的博客帖子创建唯一的ID。

user = { name: 'John Smith', email: 'js@someFakeEmailServer.com'posts: [Array Of Post IDs]
}

设置我们的测试环境 (Set up our testing environment)

For this example, we will be using Jest. Jest is a testing suite. Often, you’ll need a testing library and a separate assertion library, but Jest is an all-in-one solution.

对于此示例,我们将使用Jest。 Jest是一个测试套件。 通常,您将需要一个测试库和一个单独的断言库,但是Jest是一个多合一的解决方案。

An assertion library allows us to make assertions about our code. So in our wooden table example, our assertion is: “I expect the table to hold a 20-pound object.” In other words, I am asserting something about what the table should do.

断言库允许我们对代码进行断言。 因此,在我们的木桌示例中,我们的断言是:“我希望桌子可以容纳20磅的物体。” 换句话说,我在断言表应该做什么。

项目设置 (Project setup)

  1. Create an NPM project: npm init.

    创建一个NPM项目: npm init

  2. Create id.js and add it to the project’s root.

    创建id.js并将其添加到项目的根目录。

  3. Install Jest: npm install jest --D

    安装Jest: npm install jest --D

  4. Update the package.json test script

    更新package.json test脚本

// package.json{...other package.json stuff"scripts": {   "test": "jest" // this will run jest with "npm run test"}
}

That’s it for the project setup! We aren’t going to have any HTML or any styling. We are approaching this purely from a unit-testing standpoint. And, believe it or not, we have enough to run Jest right now.

项目设置就是这样! 我们不会有任何HTML或样式。 我们纯粹是从单元测试的角度来解决这个问题。 而且,信不信由你,我们现在有足够的钱来运营Jest。

In the command line, run our test script: npm run test.

在命令行中,运行我们的测试脚本: npm run test

You should have received an error:

您应该收到一个错误:

No tests found
In /****/3 files checked.testMatch: **/__tests__/**/*.js?(x),**/?(*.)+(spec|test).js?(x) - 0 matchestestPathIgnorePatterns: /node_modules/ - 3 matches

Jest is looking for a file name with some specific characteristics such as a .spec or .test contained within the file name.

Jest正在寻找具有某些特定特征的文件名,例如文件名中包含.spec.test

Let’s update id.js to be id.spec.js.

让我们更新id.jsid.spec.js

Run the test again

再次运行测试

You should receive another error:

您应该收到另一个错误:

FAIL  ./id.spec.js● Test suite failed to runYour test suite must contain at least one test.

A little bit better, it found the file, but not a test. That makes sense; it’s an empty file.

好一点了,它找到了文件,但没有测试。 这就说得通了; 这是一个空文件。

我们如何编写测试? (How Do We Write a Test?)

Tests are just functions that receive a couple of arguments. We can call our test with either it() or test().

测试只是接收几个参数的函数。 我们可以使用it()test()来调用测试。

it()is an alias of test().

it()test()的别名。

Let’s write a very basic test just to make sure Jest is working.

让我们编写一个非常基本的测试,以确保Jest正常运行。

// id.spec.jstest('Jest is working', () => {expect(1).toBe(1);
});

Run the test again.

再次运行测试。

PASS  ./id.spec.js✓ Jest is working (3ms)Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.254s
Ran all test suites.

We passed our first test! Let’s analyze the test and results output.

我们通过了第一个测试! 让我们分析测试和结果输出。

We pass a title or description as the first argument.

我们将标题或描述作为第一个参数。

test('Jest is Working')

test('Jest is Working')

The second argument we pass is a function where we actually assert something about our code. Although, in this case, we aren’t asserting something about our code, but rather something truthy in general that will pass, a sort of sanity check.

我们传递的第二个参数是一个函数,在该函数中我们实际上声明了有关代码的某些内容。 尽管在这种情况下,我们不是在声明代码方面的内容,而是在一般情况下会通过的真实性(一种健全性检查)。

...() => { expect(1).toBe(1) });

...() => { expect(1).toBe(1) });

This assertion is mathematically true, so it’s a simple test to ensure we’ve wired up Jest correctly.

这个断言在数学上是正确的,因此这是确保我们正确连接Jest的简单测试。

The results tell us whether the test passes or fails. It also tells us the number of tests and test suites.

结果告诉我们测试是否通过。 它还告诉我们测试和测试套件的数量。

关于组织测试的补充说明 (A side note about organizing our tests)

There is another way we could organize our code. We could wrap each test in a describe function.

我们可以用另一种方式来组织代码。 我们可以将每个测试包装在describe函数中。

describe('First group of tests', () => {test('Jest is working', () => {expect(1).toBe(1);});
});describe('Another group of tests', () => {// ...more tests here
});

describe() allows us to divide up our tests into sections:

describe()允许我们将测试分为几部分:

PASS  ./id.spec.jsFirst group of tests✓ Jest is working(4ms)✓ Some other test (1ms)Another group of tests✓ And another test✓ One more test (12ms)✓ And yes, one more test

We won’t use describe, but it is more common than not to see a describe function that wraps tests. Or even a couple of describes–maybe one for each file we are testing. For our purposes, we will just focus on test and keep the files fairly simple.

我们不会使用describe但更常见的比不看describe功能包装测试。 甚至是几个describes可能是我们正在测试的每个文件的一个。 就我们的目的而言,我们将只专注于test并使文件相当简单。

根据规格进行测试 (Testing Based on Specifications)

As tempting as it is to just sit down and start typing application logic, a well-formulated plan will make development easier. We need to define what our program will do. We define these goals with specifications.

坐下来开始键入应用程序逻辑很诱人,精心制定的计划将使开发变得更容易。 我们需要定义程序要执行的操作。 我们通过规范定义这些目标。

Our high-level specification for this project is to create a unique ID, although we should break that down into smaller units that we will test. For our small project we will use the following specifications:

我们对该项目的高级规范是创建一个唯一的ID,尽管我们应该将其分解成较小的单元进行测试。 对于我们的小型项目,我们将使用以下规范:

  1. Create a random number创建一个随机数
  2. The number is an integer.该数字是整数。
  3. The number created is within a specified range.创建的数字在指定范围内。
  4. The number is unique.该数字是唯一的。

回顾 (Recap)

  1. Jest is a testing suite and has a built-in assertion library.Jest是一个测试套件,具有内置的断言库。
  2. A test is just a function whose arguments define the test.测试只是一个函数,其参数定义了测试。
  3. Specifications define what our code should do and are ultimately what we test.规范定义了我们的代码应该做什么以及最终是我们要测试的东西。

规范1:创建一个随机数 (Specification 1: Create a Random Number)

JavaScript has a built-in function to create random numbers–Math.random(). Our first unit test will look to see that a random number was created and returned. What we want to do is use math.random() to create a number and then ensure that is the number that gets returned.

JavaScript具有内置函数来创建随机数– Math.random() 。 我们的第一个单元测试将查看是否创建并返回了一个随机数。 我们要做的是使用math.random()创建一个数字,然后确保它是要返回的数字。

So you might think we would do something like the following:

因此,您可能会认为我们会执行以下操作:

expect(our-functions-output).toBe(some-expected-value). The problem with our return value being random, is we have no way to know what to expect. We need to re-assign the Math.random() function to some constant value. This way, when our function runs, Jest replaces Math.random()with something constant. This process is called mocking. So, what we are really testing for is that Math.random()gets called and returns some expected value that we can plan for.

expect(our-functions-output).toBe(some-expected-value) 。 我们的返回值是随机的,问题在于我们无法知道期望值。 我们需要将Math.random()函数重新分配给某个常数。 这样,当我们的函数运行时,Jest用常量替换了Math.random() 。 此过程称为模拟。 因此,我们真正要测试的是调用Math.random()并返回一些我们可以计划的期望值。

Now, Jest also provides a way to prove a function is called. However, in our example, that assertion alone only assures us Math.random()was called somewhere in our code. It won’t tell us that the result of Math.random()was also the return value.

现在,Jest还提供了一种证明函数被调用的方法。 但是,在我们的示例中,仅凭断言仅可以确保我们在代码中的某处调用了Math.random() 。 它不会告诉我们Math.random()结果也是返回值。

Why would you want to mock a function? Isn’t the point to test the real code? Yes and no. Many functions contain things we cannot control, for example an HTTP request. We aren’t trying to test this code. We assume those dependencies will do what they are supposed or make pretend functions that simulate their behavior. And, in the event those are dependencies we’ve written, we will likely write separate tests for them.

为什么要模拟一个函数? 这不是测试真实代码的重点吗? 是的,没有。 许多功能包含我们无法控制的内容,例如HTTP请求。 我们不是要测试此代码。 我们假设这些依赖项将按照它们的假设进行操作,或者假装模拟其行为的函数。 而且,如果这些是我们编写的依赖项,我们可能会为它们编写单独的测试。

Add the following test to id.spec.js

将以下测试添加到id.spec.js

test('returns a random number', () => {const mockMath = Object.create(global.Math);mockMath.random = jest.fn(() => 0.75);global.Math = mockMath;const id = getNewId();expect(id).toBe(0.75);
});

分解以上测试 (Breaking the above test down)

First, we copy the global Math object. Then we change the random method to return a constant value, something we can expect. Finally, we replace the global Math object with our mocked Math object.

首先,我们复制全局Math对象。 然后,我们更改random方法以返回恒定值,这是我们可以期望的 。 最后,我们更换了全球Math与我们的嘲笑对象Math对象。

We should get an ID back from a function (that we haven't created yet–remember this TDD). Then, we expect that ID to equal 0.75–our mocked return value.

我们应该从一个函数中获得一个ID(尚未创建,请记住该TDD)。 然后,我们希望ID等于0.75-模拟的返回值。

Notice I’ve chosen to use a built-in method that Jest provides for mocking functions: jest.fn(). We could have also passed in a anonymous function instead. However, I wanted to show you this method, since there will be times that a Jest-mocked function will be required for other functionality in our tests to work .

注意,我选择使用Jest提供的内置方法来jest.fn()功能: jest.fn() 。 我们也可以传递匿名函数。 但是,我想向您展示此方法,因为有时我们的测试中的其他功能需要使用Jest-mocked函数。

Run the test: npm run test

运行测试: npm run test

FAIL  ./id.spec.js
✕ returns a random number (4ms)
● returns a random numberReferenceError: getNewId is not defined

Notice we get a reference error just like we should. Our test can’t find our getNewId().

注意,正如我们应该得到的那样,我们会得到一个参考错误。 我们的测试找不到我们的getNewId()

Add the following code above the test.

在测试上方添加以下代码。

function getNewId() {Math.random()
}

I am keeping the code and testing in the same file for simplicity. Normally, the test would be written in a separate file, with any dependencies imported as they are needed.

为了简单起见,我将代码和测试保留在同一文件中。 通常,将测试写入单独的文件中,并在需要时导入任何依赖项。

FAIL  ./id.spec.js✕ returns a random number (4ms)● returns a random numberexpect(received).toBe(expected) // Object.is equalityExpected: 0.75Received: undefined

We failed again with what is called an assertion error. Our first error was a reference error. This second error tells us it received undefined. But we called Math.random()so what happened? Remember, functions that don’t explicitly return something will implicitly return undefined. This error is a good hint that something wasn’t defined such as a variable, or, like in our case, our function isn’t returning anything.

我们再次因所谓的断言错误而失败。 我们的第一个错误是参考错误。 第二个错误告诉我们它收到的是undefined 。 但是我们叫Math.random()那么发生了什么? 请记住,未显式返回某些内容的函数将隐式返回undefined 。 该错误是一个很好的提示,表明未定义某些内容(例如变量),或者像我们这样的情况,我们的函数未返回任何内容。

Update the code to the following:

将代码更新为以下内容:

function getNewId() {return Math.random()
}

Run the test

运行测试

PASS  ./id.spec.js
✓ returns a random number (1ms)Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

Congratulations! We passed our first test.

恭喜你! 我们通过了第一个测试。

Ideally, we want to get to our assertion errors as quickly as possible. Assertion errors–specifically value assertion errors like this one, although we will touch on boolean assertions errors in a bit–give us hints to what is wrong.

理想情况下,我们希望尽快解决断言错误。 断言错误-尤其是像这样的断言错误 ,尽管我们将稍微讨论一下布尔断言错误 -但会提示我们错误之处。

规范2:我们返回的数字是整数。 (Specification 2: The number we return is an integer.)

Math.random() generates a number between 0 and 1 (not inclusive). The code we have will never generate such an integer. That’s ok though, this is TDD. We will check for an integer and then write the logic to transform our number to an integer.

Math.random()生成一个介于0和1之间(不包括在内)的数字。 我们拥有的代码永远不会生成这样的整数。 没关系,这是TDD。 我们将检查整数,然后编写逻辑以将我们的数字转换为整数。

So, how do we check if a number is an integer? We have a few options. Recall, we mocked Math.random() above, and we are returning a constant value. In fact, we are creating a real value as well since we are returning a number between 0 and 1 (not inclusive). If we were returning a string, for example, we couldn’t get this test to pass. Or if on the other hand, we were returning an integer for our mocked value, the test would always (falsely) pass.

那么,如何检查数字是否为整数? 我们有一些选择。 回想一下,我们在上面模拟了Math.random() ,并且正在返回一个常量值。 实际上,由于我们返回的是0到1之间的数字(不包括在内),因此我们也在创建一个实际值。 例如,如果返回字符串,则无法通过此测试。 或者,如果另一方面,我们为模拟值返回一个整数,则测试将始终(错误地)通过。

So a key takeaway is if you going to use mocked return values, they should be realistic so our tests return meaningful information with those values.

因此,关键要点是,如果您要使用模拟的返回值,则它们应该是现实的,以便我们的测试返回具有这些值的有意义的信息。

Another option would be to use the Number.isInteger(), passing our ID as the argument and seeing if that returns true.

另一个选择是使用Number.isInteger() ,将我们的ID作为参数传递,然后查看是否返回true。

Finally, without using the mocked values, we could compare the ID we get back with its integer version.

最后,无需使用模拟值,我们可以将返回的ID与它的整数版本进行比较。

Let’s look at option 2 and 3.

让我们看一下选项2和3。

Option 2: Using Number.isInteger()

选项2:使用Number.isInteger()

test('returns an integer', () => {const id = getRandomId();expect(Number.isInteger(id)).toBe(true);
});

The test fails as it should.

测试应有的失败。

FAIL  ./id.spec.js
✓ returns a random number (1ms)
✕ returns an integer (3ms)● returns an integer
expect(received).toBe(expected) // Object.is equalityExpected: true
Received: false

The test fails with a boolean assertion error. Recall, there are multiple ways a test might fail. We want them to fail with assertion errors. In other words, our assertion isn’t what we say it is. But even more so, we want our test to fail with value assertion errors.

测试失败,并带有布尔断言错误 。 回想一下,测试可能会以多种方式失败。 我们希望它们因断言错误而失败。 换句话说,我们的主张不是我们所说的。 但更重要的是,我们希望我们的测试因价值主张错误而失败。

Boolean assertion errors (true/false errors) don’t give us very much information, but a value assertion error does.

布尔断言错误(正确/错误)不会给我们太多信息,但是值断言却可以。

Let’s return to our wooden table example. Now bear with me, the following two statements might seem awkward and difficult to read, but they’re here to highlight a point:

让我们回到我们的木桌示例。 现在,请忍受以下两个语句可能看起来很尴尬且难以阅读,但此处仅重点说明了这一点:

First, you might assert that the table is blue [to be] true. In another assertion, you might assert the table color [to be] blue. I know, these are awkward to say and might even look like identical assertions but they're not. Take a look at this:

首先,您可以断言该表为true 。 在另一个断言中,您可以断言表的颜色为[blue] 。 我知道,这些说起来很尴尬,甚至看起来像是相同的断言,但事实并非如此。 看看这个:

expect(table.isBlue).toBe(true)

expect(table.isBlue).toBe(true)

vs

expect(table.color).toBe(blue)

expect(table.color).toBe(blue)

Assuming the table isn’t blue, the first examples error will tell us it expected true but received false. You have no idea what color the table is. We very well may have forgotten to paint it altogether. The second examples error, however, might tell us it expected blue but received red. The second example is much more informative. It points to the root of the problem much quicker.

假设表格不是蓝色,则第一个示例错误将告诉我们预期为true但收到的为false。 您不知道桌子是什么颜色。 我们可能已经完全忘记将它绘制出来了。 但是,第二个示例错误可能告诉我们它预期为蓝色,但接收到红色。 第二个例子提供了更多信息。 它更快地指出了问题的根源。

Let’s rewrite the test, using option 2, to receive a value assertion error instead.

让我们使用选项2重写测试,以接收值声明错误。

test('returns an integer', () => {const id = getRandomId();expect(id).toBe(Math.floor(id));
});

We are saying we expect the ID we get from our function to be equal to the floor of that ID. In other words, if we are getting an integer back, then the floor of that integer is equal to the integer itself.

我们说我们希望从函数中获得的ID等于该ID的下限。 换句话说,如果我们要返回整数,则该整数的下限等于整数本身。

FAIL  ./id.spec.js
✓ returns a random number (1ms)
✕ returns an integer (4ms)
● returns an integer
expect(received).toBe(expected) // Object.is equalityExpected: 0
Received: 0.75

Wow, what are the chances this function just happened to return the mocked value! Well, they are 100% actually. Even though our mocked value seems to be scoped to only the first test, we are actually reassigning the global value. So no matter how nested that re-assignment takes place, we are changing the global Math object.

哇,这个函数刚好返回模拟值的机会是多少! 好吧,他们实际上是100%。 即使我们模拟的值似乎只限于第一次测试,但实际上我们正在重新分配全局值。 因此,无论重新分配如何嵌套,我们都将更改全局Math对象。

If we want to change something before each test, there is a better place to put it. Jest offers us a beforeEach() method. We pass in a function that runs any code we want to run before each of our tests. For example:

如果我们想在每次测试之前进行更改,则有一个更好的放置方法。 Jest为我们提供了一个beforeEach()方法。 我们传入一个函数,该函数在每次测试之前都会运行我们要运行的任何代码。 例如:

beforeEach(() => {someVariable = someNewValue;
});test(...)

For our purposes, we won’t use this. But let's change our code a bit so that we reset the global Math object back to the default. Go back into the first test and update the code as follows:

为了我们的目的,我们不会使用它。 但是,让我们稍微更改一下代码,以便将全局Math对象重置为默认值。 返回第一个测试并按如下所示更新代码:

test('returns a random number', () => {const originalMath = Object.create(global.Math);const mockMath = Object.create(global.Math);mockMath.random = () => 0.75;global.Math = mockMath;const id = getNewId();expect(id).toBe(0.75);global.Math = originalMath;
});

What we do here is save the default Math object before we overwrite any of it, then reassign it after our test is complete.

我们要做的是先覆盖默认的Math对象,然后再覆盖其中的任何对象,然后在测试完成后重新分配它。

Let’s run our tests again, specifically focusing back on our second test.

让我们再次运行测试,特别是回到第二个测试。

✓ returns a random number (1ms)
✕ returns an integer (3ms)
● returns an integer
expect(received).toBe(expected) // Object.is equalityExpected: 0
Received: 0.9080890805713182

Since we’ve updated our first test to go back to the default Math object, we are truly getting a random number now. And just like the test before, we are expecting to receive an integer, or in other words, the floor of the number generated.

由于我们已经更新了第一个测试以返回到默认的Math对象,因此我们现在确实获得了一个随机数。 就像之前的测试一样,我们期望收到一个整数,换句话说,就是生成的数字的下限。

Update our application logic.

更新我们的应用程序逻辑。

function getRandomId() {return Math.floor(Math.random()); // convert to integer
}FAIL  ./id.spec.js
✕ returns a random number (5ms)
✓ returns an integer
● returns a random number
expect(received).toBe(expected) // Object.is equality
Expected: 0.75
Received: 0

Uh oh, our first test failed. So what happened?

哦,我们的第一个测试失败了。 所以发生了什么事?

Well, because we are mocking our return value. Our first test returns 0.75, no matter what. We expect, however, to get 0 (the floor of 0.75). Maybe it would be better to check if Math.random() gets called. Although, that is somewhat meaningless, because we could call Math.random()anywhere in our code, never use it, and the test still passes. Maybe, we should test whether our function returns a number. After all, our ID must be a number. Yet again, we are already testing if we are receiving an integer. And all integers are numbers; that test would be redundant. But there is one more test we could try.

好吧,因为我们在嘲笑我们的返回值。 无论如何,我们的第一个测试返回0.75。 但是,我们希望得到0(0.75的下限)。 也许最好检查一下Math.random()被调用。 虽然,这在某种意义上是没有意义的,因为我们可以在代码中的任何地方调用Math.random() ,从不使用它,并且测试仍然可以通过。 也许,我们应该测试函数是否返回数字。 毕竟,我们的ID必须是数字。 再一次,我们已经在测试是否要接收整数。 所有整数都是数字; 该测试将是多余的。 但是,我们可以尝试另一项测试。

When it is all said and done, we expect to get an integer back. We know we will use Math.floor() to do so. So maybe we can check if Math.floor() gets called with Math.random() as an argument.

总而言之,我们期望得到一个整数。 我们知道我们将使用Math.floor()这样做。 因此,也许我们可以检查Math.floor()是否以Math.random()作为参数被调用。

test('returns a random number', () => {jest.spyOn(Math, 'floor'); // <--------------------changedconst mockMath = Object.create(global.Math); const globalMath = Object.create(global.Math);mockMath.random = () => 0.75;global.Math = mockMath;const id = getNewId();getNewId(); //<------------------------------------changedexpect(Math.floor).toHaveBeenCalledWith(0.75); //<-changedglobal.Math = globalMath;
});

I’ve commented the lines we changed. First, move your attention towards the end of the snippet. We are asserting that a function was called. Now, go back to the first change: jest.spyOn(). In order to watch if a function has been called, jest requires us to either mock that function, or spy on it. We’ve already seen how to mock a function, so here we spy on Math.floor(). Finally, the other change we’ve made was to simply call getNewId() without assigning its return value to a variable. We are not using the ID, we are simply asserting it calls some function with some argument.

我评论了我们更改的内容。 首先,将注意力转移到摘要的末尾。 我们声称已经调用了一个函数。 现在,返回第一个更改: jest.spyOn() 。 为了观察某个函数是否被调用,开玩笑要求我们要么模拟该函数,要么对其进行监视。 我们已经了解了如何模拟函数,因此在这里我们窥探Math.floor() 。 最后,我们所做的另一个更改是简单地调用getNewId()而不将其返回值分配给变量。 我们没有使用ID,只是断言它使用某些参数调用了某些函数。

Run our tests

运行我们的测试

PASS  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integerTest Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Congratulations on a second successful test.

祝贺第二次成功测试。

规格3:该数字在指定范围内。 (Specification 3: The number is within a specified range.)

We know Math.random() returns a random number between 0 and 1 (not inclusive). If the developer wants to return a number between 3 and 10, what could she do?

我们知道Math.random()返回0到1(不包括)之间的随机数。 如果开发人员想要返回3到10之间的数字,她该怎么办?

Here is the answer:

答案是:

Math.floor(Math.random() * (max — min + 1))) + min;

Math.floor(Math.random() * (max — min + 1))) + min;

The above code will produce a random number in a range. Let’s look at two examples to show how it works. I’ll simulate two random numbers being created and then apply the remainder of the formula.

上面的代码将产生一个范围内的随机数。 让我们看两个例子来说明它是如何工作的。 我将模拟创建两个随机数,然后应用公式的其余部分。

Example: A number between 3 and 10. Our random numbers will be .001 and .999. I’ve chosen the extreme values as the random numbers so you could see the final result stays within the range.

示例: 3到10之间的数字。我们的随机数将是.001和.999。 我选择了极值作为随机数,因此您可以看到最终结果保持在该范围内。

0.001 * (10-3+1) + 3 = 3.008 the floor of that is 3

0.001 * (10-3+1) + 3 = 3.008的底限是3

0.999 * (10-3+1) + 3 = 10.992 the floor of that is 10

0.999 * (10-3+1) + 3 = 10.992的底限是10

Let’s write a test

让我们写一个测试

test('generates a number within a specified range', () => {const id = getRandomId(10, 100);expect(id).toBeLessThanOrEqual(100);expect(id).toBeGreaterThanOrEqual(10);
});FAIL  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer (1ms)
✕ generates a number within a specified range (19ms)● generates a number within a specified range
expect(received).toBeGreaterThanOrEqual(expected)Expected: 10
Received: 0

The floor of Math.random() will always be 0 until we update our code. Update the code.

在我们更新代码之前, Math.random()的下限始终为0。 更新代码。

function getRandomId(min, max) {return Math.floor(Math.random() * (max - min + 1) + min);
}FAIL  ./id.spec.js
✕ returns a random number (5ms)
✓ returns an integer (1ms)
✓ generates a number within a specified range (1ms)● returns a random numberexpect(jest.fn()).toHaveBeenCalledWith(expected)Expected mock function to have been called with:0.75 as argument 1, but it was called with NaN.

Oh no, our first test failed again! What happened?

哦,不,我们的第一次测试再次失败! 发生了什么?

Simple, our test is asserting that we are calling Math.floor() with 0.75. However, we actually call it with 0.75 plus and minus a max and min value that isn’t yet defined. Here we will re-write the first test to include some of our new knowledge.

很简单,我们的测试断言我们用0.75调用Math.floor() 。 但是,我们实际上用0.75加上和减去一个尚未定义的最大值和最小值来调用它。 在这里,我们将重新编写第一个测试,以包含一些新知识。

test('returns a random number', () => {jest.spyOn(Math, 'floor');const mockMath = Object.create(global.Math);const originalMath = Object.create(global.Math);mockMath.random = () => 0.75;global.Math = mockMath;const id = getNewId(10, 100);expect(id).toBe(78);global.Math = originalMath;
});PASS  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer
✓ generates a number within a specified range (1ms)Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

We’ve made some pretty big changes. We’ve passed some sample numbers into our function (10, and 100 as minimum and maximum values), and we’ve changed our assertion once again to check for a certain return value. We can do this because we know if Math.random() gets called, the value is set to 0.75. And, when we apply our min and max calculations to 0.75 we will get the same number each time, which in our case is 78.

我们做了一些相当大的改变。 我们已经将一些样本编号传递到函数中(最小和最大值分别为10和100),并且我们再次更改了断言以检查某个返回值。 我们之所以这样做,是因为我们知道是否调用了Math.random() ,该值设置为0.75。 而且,当我们将最小值和最大值计算为0.75时,每次都会获得相同的数字,在我们的例子中为78。

Now we have to start wondering if this is even a good test. We’ve had to go back in and mold our test to fit our code. That goes against the spirit of TDD a bit. TDD says to change your code to make the test pass, not to change the test to make the test pass. If you find yourself trying to fix tests so they pass, that may be a sign of a bad test. Yet, I’d like to leave the test in here, as there are a couple of good concepts. However, I urge you to consider the efficacy of a test such as this, as well as a better way to write it, or if it’s even critical to include at all.

现在我们必须开始怀疑这是否是一个很好的测试。 我们必须回过头来修改我们的测试以适合我们的代码。 这有点违背了TDD的精神。 TDD表示更改代码以通过测试,而不是更改测试以通过测试。 如果您发现自己试图修复测试以使其通过,则可能表明测试不佳。 但是,我想在这里进行测试,因为这里有两个很好的概念。 但是,我敦促您考虑这样的测试的有效性,以及编写它的更好方法,或者考虑是否包括所有测试至关重要。

Let’s return to our third test which was generating a number within a range.

让我们回到第三个测试,它生成一个范围内的数字。

We see it has passed, but we have a problem. Can you think of it?

我们看到它已经过去了,但是有一个问题。 你能想到吗?

The question I am wondering is whether we just get lucky? We only generated a single random number. What are the chances that number just happened to be in the range and pass the test?

我想知道的问题是,我们是否很幸运? 我们只生成了一个随机数。 这个数字刚好在范围内并通过测试的机会是什么?

Fortunately here, we can mathematically prove our code works. However, for fun (if you can call it fun), we will wrap our code in a for loop that runs 100 times.

幸运的是,我们可以在数学上证明我们的代码有效。 但是,为了好玩(如果您可以称其为有趣),我们会将代码包装在一个运行100次的for loop中。

test('generates a number within a defined range', () => {for (let i = 0; i < 100; i ++) {const id = getRandomId(10, 100);    expect(id).toBeLessThanOrEqual(100);expect(id).toBeGreaterThanOrEqual(10);expect(id).not.toBeLessThan(10);expect(id).not.toBeGreaterThan(100);}
});

I added a few new assertions. I use the .not only to demonstrate other Jest API’s available.

我添加了一些新的断言。 我用的.not只是为了演示其他玩笑API的使用。

PASS  ./id.spec.js✓ is working (2ms)✓ Math.random() is called within the function (3ms)✓ receives an integer from our function (1ms)✓ generates a number within a defined range (24ms)Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.806s

With 100 iterations, we can feel fairly confident our code keeps our ID within the specified range. You could also purposely try to fail the test for added confirmation. For example, you could change one of the assertions to not expect a value greater than 50 but still pass in 100 as the maximum argument.

通过100次迭代,我们可以确信我们的代码将ID保持在指定范围内。 您也可以有意地尝试使测试失败,以增加确认。 例如,你可以改变断言一个期望的值大于50,但在100还通过为最大理由。

可以在一个测试中使用多个断言吗? (Is it ok to use multiple assertions in one test?)

Yes. That isn’t to say you shouldn’t attempt to reduce those multiple assertions to a single assertion that is more robust. For example, we could rewrite our test to be more robust and reduce our assertions to just one.

是。 这并不是说您不应该尝试将这些多个断言简化为更可靠的单个断言。 例如,我们可以重写测试以使其更健壮,并将断言减少为仅一个。

test('generates a number within a defined range', () => {const min = 10;const max = 100;const range = [];for (let i = min; i < max+1; i ++) {range.push(i);}for (let i = 0; i < 100; i ++) {const id = getRandomId(min, max);expect(range).toContain(id);}
});

Here, we created an array that contains all the numbers in our range. We then check to see if the ID is in the array.

在这里,我们创建了一个数组,其中包含我们范围内的所有数字。 然后,我们检查ID是否在数组中。

规格4:数字是唯一的 (Specification 4: The number is unique)

How can we check if a number is unique? First, we need to define what unique to us means. Most likely, somewhere in our application, we would have access to all ID’s being used already. Our test should assert that the number that is generated is not in the list of current IDs. There are a few different ways to solve this. We could use the .not.toContain() we saw earlier, or we could use something with index.

我们如何检查数字是否唯一? 首先,我们需要定义什么对我们意味着什么。 最有可能的是,在我们应用程序中的某个地方,我们将可以访问所有已在使用的ID。 我们的测试应断言所生成的数字不在当前ID的列表中。 有几种不同的方法可以解决此问题。 我们可以使用前面看到的.not.toContain() ,也可以使用带有index东西。

指数() (indexOf())

test('generates a unique number', () => {const id = getRandomId();const index = currentIds.indexOf(id);expect(index).toBe(-1);
});

array.indexOf() returns the position in the array of the element you pass in. It returns -1 if the array doesn’t contain the element.

array.indexOf()返回您传入的元素在数组中的位置。如果数组不包含该元素,则返回-1

FAIL  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer
✓ generates a number within a defined range (25ms)
✕ generates a unique number (10ms)● generates a unique numberReferenceError: currentIds is not defined

The test fails with a reference error. currentIds is not defined. Let's add an array to simulate some ID’s that might already exist.

测试失败,并出现参考错误。 未定义currentIds 。 让我们添加一个数组来模拟一些可能已经存在的ID。

const currentIds = [1, 3, 2, 4];

Re-run the test.

重新运行测试。

PASS  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer
✓ generates a number within a defined range (27ms)
✓ generates a unique numberTest Suites: 1 passed, 1 totalTests:       4 passed, 4 total

While the test passes, this should once again raise a red flag. We have absolutely nothing that ensures the number is unique. So, what happened?

测试通过时,这应该再次发出红色标记。 我们绝对没有任何东西可以确保数字是唯一的。 所以发生了什么事?

Again, we are getting lucky. In fact, your test may have failed. Although if you ran it over and over, you’d likely get a mix of both with far more passes than failures due to the size of currentIds.

再次,我们很幸运。 实际上, 您的测试可能失败了。 尽管如果反复运行它,由于currentIds的大小,您可能会混合使用两种方法,并且通过次数比失败多得多。

One thing we could try is to wrap this in a for loop. A large enough for loop would likely cause us to fail, although it would be possible they all pass. What we could do is check to see that our getNewId() function could somehow be self-aware when a number is or is not unique.

我们可以尝试的一件事是将其包装在for loop 。 足够大的for loop可能会导致我们失败,尽管它们可能全部通过。 我们可以做的是检查数字是否唯一时, getNewId()函数可以以某种方式自我感知。

For example. we could set currentIds = [1, 2, 3, 4, 5]. Then call getRandomId(1, 5) . Our function should realize there is no value it can generate due to the constraints and pass back some sort of error message. We could test for that error message.

例如。 我们可以设置currentIds = [1, 2, 3, 4, 5] 。 然后调用getRandomId(1, 5) 。 我们的函数应该意识到由于约束条件它无法生成任何值,并传回某种错误消息。 我们可以测试该错误消息。

test('generates a unique number', () => {mockIds = [1, 2, 3, 4, 5];let id = getRandomId(1, 5, mockIds);expect(id).toBe('failed');id = getRandomId(1, 6, mockIds);expect(id).toBe(6);
});

There are a few things to notice. There are two assertions. In the first assertion, we expect our function to fail since we constrain it in a way that it shouldn’t return any number. In the second example, we constrain it in a way where it should only be able to return 6.

有一些注意事项。 有两个断言。 在第一个断言中,我们期望函数失败,因为我们以不应该返回任何数字的方式对其进行约束。 在第二个示例中,我们以只能返回6的方式对其进行约束。

FAIL  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer (1ms)
✓ generates a number within a defined range (24ms)
✕ generates a unique number (6ms)● generates a unique numberexpect(received).toBe(expected) // Object.is equalityExpected: "failed"
Received: 1

Our test fails. Since our code isn’t checking for anything or returning failed, this is expected. Although, it is possible your code received a 2 through 6.

我们的测试失败。 由于我们的代码不检查任何内容或返回failed ,因此可以预期。 虽然,您的代码可能会收到2到6。

How can we check if our function can’t find a unique number?

我们如何检查我们的功能不能找到一个唯一的号码?

First, we need to do some sort of loop that will continue creating numbers until it finds one that’s valid. At some point though, if there are no valid numbers, we need to exit the loop so we avoid an infinite loop situation.

首先,我们需要执行某种循环,该循环将继续创建数字,直到找到有效的数字为止。 但是在某个时候,如果没有有效的数字,我们需要退出循环,从而避免出现无限循环的情况。

What we will do is keep track of each number we’ve created, and when we’ve created every number we can, and none of those numbers pass our unique check, we will break out of the loop and provide some feedback.

我们要做的是跟踪我们创建的每个数字,当我们创建了每个数字后,并且这些数字都没有通过我们的独特检查,我们将跳出循环并提供一些反馈。

function getNewId(min = 0, max = 100, ids =[]) {let id;do {id = Math.floor(Math.random() * (max - min + 1)) + min;} while (ids.indexOf(id) > -1);return id;
}

First, we refactored getNewId() to include a parameter that is a list of current ID’s. In addition, we updated our parameters to provide default values in the event they aren’t specified.

首先,我们将getNewId()重构为包括一个参数,该参数是当前ID的列表。 此外,我们更新了参数,以在未指定默认值的情况下提供默认值。

Second, we use a do-while loop since we don’t know how many times it will take to create a random number that is unique. For example, we could specify a number from 1to 1000 with the only number unavailable being 7. In other words, our current ID’s only has a single 7 in it. Although our function has 999 other numbers to choose from, it could theoretically produce the number 7 over and over again. While this is very unlikely, we use a do-while loop since we are not sure how many times it will run.

其次,由于不知道创建唯一的随机数需要多少次,因此我们使用了do-while循环。 例如,我们可以指定一个1到1000之间的数字, 唯一不可用的数字是7。换句话说,我们当前的ID中只有一个7。 尽管我们的函数有999个其他数字可供选择,但理论上它可以一次又一次地产生数字7。 尽管这不太可能发生,但是我们使用了do-while循环,因为我们不确定它将运行多少次。

Additionally, notice we break out of the loop when our ID is unique. We determine this with indexOf().

此外,请注意我们打破循环出来的时候我们的ID 唯一的。 我们用indexOf()确定。

We still have a problem, with the code currently how it is, if there are no numbers available, the loop will continue to run and we will be in an infinite loop. We need to keep track of all the numbers we create, so we know when we’ve run out of numbers.

我们仍然有一个问题,当前的代码是这样,如果没有可用的数字,循环将继续运行,并且我们将处于无限循环中。 我们需要跟踪我们创建的所有数字,因此我们知道何时数字用完。

function getRandomId(min = 0, max = 0, ids =[]) {let id;let a = [];do {id = Math.floor(Math.random() * (max - min + 1)) + min;if (a.indexOf(id) === -1) {a.push(id);}if (a.length === max - min + 1) {if (ids.indexOf(id) > -1) {return 'failed';}}} while (ids.indexOf(id) > -1);return id;
}

Here is what we did. We solve this problem by creating an array. And every time we create a number, add it to the array (unless it already in there). We know we’ve tried every number at least once when the length of that array is equal to the range we’ve chosen plus one. If we get to that point, we’ve created the last number. However, we still want to make sure the last number we created doesn’t pass the unique test. Because if it does, although we want the loop to be over, we still want to return that number. If not, we return “failed”.

这就是我们所做的。 我们通过创建一个数组来解决这个问题。 每次我们创建一个数字时,都将其添加到数组中(除非已经存在)。 我们知道,当该数组的长度等于我们选择的范围加一时,我们至少尝试了每个数字一次。 如果到了这一点,我们就创建了最后一个数字。 但是,我们仍然要确保我们创建的最后一个数字未通过唯一测试。 因为如果这样做,尽管我们希望循环结束,但我们仍想返回该数字。 如果不是,我们返回“失败”。

PASS  ./id.spec.js
✓ returns a random number (1ms)
✓ returns an integer (1ms)
✓ generates a number within a defined range (24ms)
✓ generates a unique number (1ms)Test Suites: 1 passed, 1 totalTests:       4 passed, 4 total

Congratulations, we can ship our ID generator and make our millions!

恭喜,我们可以发货我们的ID生成器,使我们的收入达到数百万美元!

结论 (Conclusion)

Some of what we did was for demonstration purposes. Testing whether our number was within a specified range is fun, but that formula can be mathematically proven. So a better test might be to make sure the formula is called.

我们所做的一些是出于演示目的。 测试我们的数字是否在指定范围内很有趣,但是该公式可以通过数学证明。 因此,更好的测试可能是确保调用该公式。

Also, you could get more creative with the random ID generator. For example, if it can’t find a unique number, the function could automatically increase the range by one.

另外,您可以通过随机ID生成器获得更多创意。 例如,如果找不到唯一编号,则该功能可以自动将范围增加一。

One other thing we saw was how our tests and even specifications might crystalize a bit as we test and refactor. In other words, it would be silly to think nothing will change throughout the process.

我们看到的另一件事是,在我们测试和重构时,我们的测试甚至规范可能如何变得明确。 换句话说,认为在整个过程中什么都不会改变是很愚蠢的。

Ultimately, test-driven development provides us with a framework to think about our code at a more granular level. It is up to you, the developer, to determine how granular you should define your tests and assertions. Keep in mind, the more tests you have, and the more narrowly focused your tests are, the more tightly coupled they become with your code. This might cause a reluctance to refactor because now you must also update your tests. There is certainly a balance in the number and granularity of your tests. The balance is up to you, the developer, to figure out.

最终,测试驱动的开发为我们提供了一个框架,可以在更细粒度的层次上考虑我们的代码。 由开发人员来决定要定义测试和断言的粒度。 请记住,您拥有的测试越多,测试的重点越狭窄,它们与您的代码的耦合就越紧密。 这可能会导致不愿重构,因为现在您还必须更新测试。 测试的数量和粒度肯定是平衡的。 余额由开发人员来确定。

Thanks for reading!

谢谢阅读!

woz

沃兹

翻译自: https://www.freecodecamp.org/news/an-introduction-to-test-driven-development-c4de6dce5c/

测试驱动开发 测试前移

测试驱动开发 测试前移_测试驱动开发简介相关推荐

  1. 测试驱动开发 测试前移_测试驱动开发–双赢策略

    测试驱动开发 测试前移 敏捷从业人员谈论测试驱动开发 (TDD),所以许多关心代码质量和可操作性的开发人员也是如此. 我曾几何时,不久前设法阅读了有关TDD的文章. 据我了解,TDD的关键是: 编写测 ...

  2. 测试驱动开发 测试前移_测试驱动的开发可能看起来是工作的两倍-但无论如何您都应该这样做...

    测试驱动开发 测试前移 by Navdeep Singh 通过Navdeep Singh 测试驱动的开发可能看起来是工作的两倍-但无论如何您都应该这样做 (Test-driven developmen ...

  3. 测试驱动开发 测试前移_测试驱动陷阱,第2部分

    测试驱动开发 测试前移 单元测试中单元的故事 在本文的上半部分 ,您可能会看到一些不好但很受欢迎的测试示例. 但是我不是一个专业的批评家(也被称为"巨魔"或"仇恨者&qu ...

  4. 测试驱动开发 测试前移_测试驱动开发:它是什么,什么不是。

    测试驱动开发 测试前移 by Andrea Koutifaris 由Andrea Koutifaris Test driven development has become popular over ...

  5. 测试驱动开发 测试前移_我如何以及为什么认为测试驱动开发值得我花时间

    测试驱动开发 测试前移 by Ronauli Silva 通过罗纳利·席尔瓦(Ronauli Silva) I first read about test driven development (TD ...

  6. 测试驱动开发 测试前移_为什么测试驱动的开发有用?

    测试驱动开发 测试前移 有关如何更有效地应用TDD的技巧,以及为什么它是一种有价值的技术 (Tips on how to apply TDD more efficiently, and why it' ...

  7. 植发搞笑图片_植发失败实例:头发没长出来还更秃了?詹姆斯也没能幸免,可怕...

    原标题:植发失败实例:头发没长出来还更秃了?詹姆斯也没能幸免,可怕 脱发其实是一种病,当你患有脱发症时主要面对的问题就是发际线后移.头发成片脱落等 如果不及时治疗,这些脱发面积就会越来越大,最终导致秃 ...

  8. 植发搞笑图片_植发失败案例实录!历时几个月却迎来头发尽毁,后果太可怕了...

    脱发其实是一种病,当你患有脱发症时主要面对的问题就是发际线后移.头发成片脱落等 如果不及时治疗,这些脱发面积就会越来越大,最终导致秃顶 这不仅使日常生活充满压力,而且还会损害一个人的自信心 这时,你就 ...

  9. 植发搞笑图片_植发后反而更秃了?发际线直接变成地中海,不是一般的坑啊

    脱发其实是一种病,当你患有脱发症时主要面对的问题就是发际线后移.头发成片脱落等 如果不及时治疗,这些脱发面积就会越来越大,最终导致秃顶 这不仅使日常生活充满压力,而且还会损害一个人的自信心 这时,你就 ...

最新文章

  1. python pip升级 指向不同python版本
  2. EOJ_1064_树的层号表示法
  3. python3安装json库-python库json快速入门
  4. c# 插入数据到 uniqueidentifier_每天5分钟用C#学习数据结构(16)二叉树 Part 2
  5. ajax 防止用户反复提交
  6. kubernetesV1.13.1一键部署脚本(k8s自动部署脚本)
  7. TimesTen更改CacheGroup管理用户ORACLE结束和TT结束password【TimesTen操作和维修基地】...
  8. C语言下取整下半个方括号,c语言易错知识点总结[工作范文](28页)-原创力文档...
  9. TCP/IP的全部IP协议号
  10. Linux系统中安装软件的三种方法
  11. JAVA动态申请数组
  12. 来了来了,2020 首场 Meetup ,可!
  13. CentOS7 部署 RAID 磁盘阵列
  14. 创新创业大赛的目的是什么?为什么要参加创新创业大赛?
  15. AprilTag: A robust and flexible visual fiducial system论文解读
  16. 模糊查询银行卡号mysql_mysql模糊查询
  17. wchar_t的使用
  18. 产业区块链一周新动态
  19. 流量监控服务器应该位置在哪里,搭建cacti流量监控服务器.pdf
  20. 从浏览器地址栏输入url到请求返回发生了什么?

热门文章

  1. 金蝶中间件部署报栈溢出_京东618压测时自研中间件暴露出的问题,压测级别数十万/秒...
  2. Redux 入门教程(二):中间件与异步操作
  3. JS同时上传表单图片和表单信息并把上传信息存入数据库,带php后端源码
  4. 图解5G NR帧结构
  5. GMTC 大前端时代前端监控的最佳实践
  6. android:themes.xml
  7. mysql======基本的数据查询(1)
  8. Visual Studio Remote Debugger(for 2005/2008) .net远程调试转
  9. 《Python和Pygame游戏开发指南》——2.16 pygame.display.update()函数
  10. Ios生产证书申请(含推送证书)