Recently, the company is promoting unit testing, but some colleagues only know about unit testing, or even do not know much about it. So there are barriers to pushing unit testing, not just at the human level, but at the infrastructure level. It is hoped that this article, on the one hand, will deepen your understanding of the best practices of front-end testing, on the other hand, it can be used as a manual for reference in daily development. This article will be updated and we look forward to your participation.
If you’re not sure about the front-end test, you can read my popular science essay at the end of this article. If you already know something about front-end testing and want to learn more about it and how to write better unit tests, let’s get started.
Writing test-friendly Code
This is a very broad topic, this paper tries to elaborate this huge and vague topic from several specific entry points.
Pure Function
For pure functions [1] see the introduction to a functional tutorial I wrote earlier.
Simply put, a pure function is a function in mathematics. There are two benefits:
- Assertion is easy. (Derivability)
- I can execute the test case multiple times, no matter the order. (No side effects)
Let me give you an example, which is a slightly more advanced technique. But once you understand the intent, you’ll see how simple the idea is.
const app = {
name: `lucifer's site`
start(html) {
document.querySelector('#app').innerHTM = html;
}
}
app.start(<div>inner</div>);
Copy the code
To test the above code, you first need to simulate the Document in the Node environment.
What if I could write it another way?
const app = {
name: `lucifer's site`
start(querySelector, html) {
querySelector('#app').innerHTM = html;
}
}
app.start(document.querySelector, <div>inner</div>);
Copy the code
This makes it easy to emulate querySelector. eg:
// .test.js
import app from "./app";
app.start((a)= > <div id="app">lucifer</div>, <div>inner</div>);
Copy the code
If you’re familiar with this approach, you probably know it by its name, inversion of control, or IoC.
Single Responsibility Principle
If a function has more than one responsibility. So what does that do to our test?
If I have A function f, it has functions A and B.
- The input of A is called IA and the output is called OA.
- The input of B is called IB and the output is called ob.
So the cyclomatic complexity of F is going to increase a lot, specifically.
- If function A is related to function B, the increase in the length of its test cases is the Cartesian product.
- If A function is independent of B function, the length of its test cases grows linearly.
eg:
function math(a, b, operator) {
if (operator === "+") return a + b;
if (operator === "-") return a - b;
if (operator === "*") return a * b;
if (operator === "/") return a / b;
}
Copy the code
The code above has four functions that are independent of each other. The growth of test cases is linear, that is, the number of test cases remains the same after splitting into four functions, but the cyclomatic complexity of a single function decreases, even though the overall software complexity does not decrease.
If the four functions are coupled, the consequences are even worse. In this case, splitting multiple pieces of functionality is no longer the solution. At this point, the function needs to be disassembled again until the sub-function blocks are independent of each other.
Wrting Deadly Simply Description
I’m going to give you a simple criterion.
When this test fails, others can simply read the error message and know what is wrong.
For example, it’s good to write:
describe(`math -> add`, () = > {
it("3 + 2 should equal to 5", () = > {
expect(3 + 2).to.be.equal(5);
});
});
Copy the code
And that’s not good:
describe(`math -> add`, () = > {
it("add two numbers", () = > {
expect(3 + 2).to.be.equal(5);
});
});
Copy the code
The examples I’ve given you may be dismissive, but when you measure them against my standards you’ll find that many use cases fail.
Logic Coverage
A lot of people focus on physical coverage of unit tests, such as line coverage, file coverage, etc., and people tend to ignore logical coverage.
eg:
// a.js
export default (a, b) => a / b
// a.test.js
import divide './a.js'
describe(`math -> divide`, () = > {
it("2 / 2 should be 1", () = > {
expect(divide(2.2)).to.be(1);
});
});
Copy the code
As mentioned above, physical coverage can be 100%, but obviously logical coverage cannot. Because it doesn’t even include the simplest dividend that can’t be zero.
A more formal example would be:
// a.js
export default (a, b) => {
if (b === 0 or b === 0) throw new Error('dividend should not be zero! ')
if (Number(a) ! == a ||Number(b)=== b) throw new Error('Divisor and dividend should be number, but got${a, b}`)
return a / b
}
// a.test.js
import divide './a.js'
describe(`math -> divide`, () = > {
it("when dividend it zero, there should throw an corresponding eror", () = > {
expect(divide(3.0)).toThrowError(/dividend should not be zero/);
});
it("when dividend it zero, there should throw an corresponding eror", () = > {
expect(divide(3.'f')).toThrowError(/divisor and dividend should be number/);
});
it("2 / 2 should be 1", () = > {
expect(divide(2.2)).to.be(1);
});
});
Copy the code
Logic rigor works both ways. On the one hand, it makes your test cases more rigorous and watertight. On the other hand, the more rigorous your test cases are, the more rigorous your code will be. The Divide method above was added by me based on the feedback of test cases.
However, my above test is still very illogical, such as:
- Large overflow is not considered.
- No consideration for infinite repeating decimals.
There are so many edge cases for such a simple division, and it would be even more complicated if it were our actual business. So “write well” tests are never easy.
Add Lint (Add Linting) to tests
The test code also needs Lint. In addition to some lint rules from the source code, tests should include some unique rules.
For example, your test code just runs through the code without making any assertions. Or direct assertions expect(true.to.be(true)) should not be allowed.
For example, using incongruence in assertions is also bad practice.
For another example, use toBeNull() assertion instead of:
expect(null).toBe(null);
expect(null).toEqual(null);
expect(null).toStrictEqual(null);
Copy the code
.
Lint is also required for test code, and it should have extra special rules than the code being tested to avoid the “rot problem” of the test code.
CI
Local Testing (Local CI)
You can test only modified files, eg:
jest -o
Copy the code
Phased testing (Tags)
We can classify test cases according to certain classification criteria.
For example, I divide use cases into IO types and non-IO types based on whether they test for IO. Then I can only execute non-IO types when I commit, so the feedback is faster. Wait until I push to the remote to do a full operation.
eg:
describe(`"face swiping" -> alipay #io`, () = > {
it("it should go to http://www.alipay.com/identify when user choose alipay", () = > {
// simulate click
// do heavy io
// expect
});
});
Copy the code
We can do that
jest -t = "#io";
Copy the code
Similarly, I can shard use cases by other latitudes, such as various business latitudes. This is very obvious when the business reaches a certain scale. eg:
jest -t = "[#io|#cold|#biz]";
Copy the code
The above will test only use cases with one or more of the IO, Cold, and BIZ tags.
❝
Folders and file names are themselves tags, and proper use can save a lot of work.
❞
Framework dependent (Framework)
A lot of the questions people ask are how to test views and how to test code in a particular framework.
Vue
A typical Vue project might have the following file types:
- html
- vue
- js
- ts
- json
- css
- Pictures, audio and video and other media resources
How do you test them? JS and TS we’ll talk about for the moment, but it’s not very framework relevant. Here we are concerned with frame-specific VUE files and view-specific files. “There is no need to test json, images, audio and video.”
So how do you test HTML, VUE, and CSS files? Most of the time, people are using CSR, and HTML is just a dummy file with no testing value. CSS, if you want to test, there are only two cases, one is to test CSSOM, the other is to test the content of the rendered tree. The render tree is usually tested. Why is that? I’ll leave it to you to think about it. So this article focuses on the VUE file and the test of the render tree.
In fact, the VUE file exports a vue constructor and completes the instantiation and mounting process when appropriate. When it is rendered, the template tag and the style tag will be passed along. Of course, there is some complicated logic in this process, which is not the focus of this article, so I will not extend it.
So, the main focus of vUE framework based application testing is the render tree itself. It’s the same if you use another frame, or if you don’t use a frame.
The difference is that VUE is a data-driven framework.
(props) => view;
Copy the code
So should we just test different props combinations and show the view we want?
Yes and no. Let’s say yes. So our question becomes:
- How to combine the right props
- How do I assert that the view renders correctly
For the first question, this is something to consider when designing components. For the second question, the answer is vue-test-utils.
Vue-test-utils itself solves this problem if I think of an app as an organism of components (components and communication collaboration between components) and components as functions. The core function of vue-test-utils is:
- To help you execute these functions.
- Change the state inside the function.
- Trigger communication between functions.
- .
The Wrapper for vue-test-utils does both setProps and Assert. Vue-test-utils also helps you with things like how to test nested components (like function call stacks), how to mock props, routers, etc.
In short, it’s like a pair of invisible hands, “helping you initialize, mount, update, uninstall your app, and providing assertion mechanisms, directly or indirectly.” More can be found at https://vue-test-utils.vuejs.org/
The above is based on the fact that all we need to do is test different props combinations to see if we want the view to be presented. However, vue abstracts it as a function, but note that this function is far from the pure functions I mentioned above. Even the famously function-friendly React doesn’t do this.
That said, you also need to consider side effects. At this point, this is a departure from the best practices I mentioned above. But frameworks that actually remove all side effects are not very popular, such as CycleJS and ELM. So we have to accept that. We can’t avoid it, but we can limit it to the extent that we can control it. A typical technique is the sandbox mechanism, which is also beyond the scope of this article, so we won’t cover it.
React
TODO
Others
Make it Red, Make it Green
This is the essence of test-driven development.
-
Write the use cases first, regardless of whether they are flying or not, write the test cases first, and define the problem boundaries.
-
And then turn the red ones green one by one.
-
With the techniques I mentioned above, do continuous integration. What are the test cases that you can execute when you type, and what are the test cases that you can execute when you commit to the local repository?
Reference = Reference
- Front-end test essay written two years ago[2]
- eslint-plugin-jest
Reference
[1]
Functional tutorial: https://github.com/azl397985856/functional-programming
[2]
Two years ago write the front end of the test article: https://github.com/azl397985856/frontend-test