269 lines
17 KiB
HTML
269 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"
|
|
/>
|
|
</head>
|
|
<style>
|
|
/* https://github.com/sindresorhus/github-markdown-css */
|
|
.markdown-body {
|
|
box-sizing: border-box;
|
|
min-width: 200px;
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
padding: 45px;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.markdown-body {
|
|
padding: 15px;
|
|
}
|
|
}
|
|
</style>
|
|
<body class="markdown-body">
|
|
<a href="index">Index</a>
|
|
<h1><a id="user-content-speed-up-writing-go-test-asap" class="anchor" aria-hidden="true" href="#speed-up-writing-go-test-asap"><span aria-hidden="true" class="octicon octicon-link"></span></a>Speed up writing Go test ASAP</h1>
|
|
<p>Imagine your project currently have 0% unit test code coverage.
|
|
And your boss keep pushing it to 80% or even 90%?
|
|
What do you do?
|
|
Give up?</p>
|
|
<p>What if I tell you there is a way?
|
|
Not entirely cheating but ... you know, there is always trade off.</p>
|
|
<p>If your purpose is to test carefully all path, check if all return is correctly.
|
|
Sadly this post is not for you, I guess.
|
|
If you only want good number on test coverage, with minimum effort as possible, I hope this will show you some idea you can use :)</p>
|
|
<p>In my opinion, unit test is not that important (like must must have).
|
|
It's just make sure your code is running excatly as you intent it to be.
|
|
If you don't think about edge case before, unit test won't help you.</p>
|
|
<h2><a id="user-content-first-rewrite-the-impossible-to-test-out" class="anchor" aria-hidden="true" href="#first-rewrite-the-impossible-to-test-out"><span aria-hidden="true" class="octicon octicon-link"></span></a>First, rewrite the impossible (to test) out</h2>
|
|
<p>When I learn programming, I encounter very interesting idea, which become mainly my mindset when I dev later.
|
|
I don't recall it clearly, kinda like: "Don't just fix bugs, rewrite it so that kind of bugs will not appear again".
|
|
So in our context, there is some thing we hardly or can not write test in Go.
|
|
My suggestion is don't use that thing.</p>
|
|
<p>In my experience, I can list a few here:</p>
|
|
<ul>
|
|
<li>Read config each time call func (<code>viper.Get...</code>). You can and you should init all config when project starts.</li>
|
|
<li>Not use Dependency Injection (DI). There are too many posts in Internet tell you how to do DI properly.</li>
|
|
<li>Use global var (Except global var <code>Err...</code>). You should move all global var to fields inside some struct.</li>
|
|
</ul>
|
|
<h2><a id="user-content-let-the-fun-writing-test-begin" class="anchor" aria-hidden="true" href="#let-the-fun-writing-test-begin"><span aria-hidden="true" class="octicon octicon-link"></span></a>Let the fun (writing test) begin</h2>
|
|
<p>If you code Go long enough, you know table driven tests and how is that so useful.
|
|
You set up test data, then you test.
|
|
Somewhere in the future, you change the func, then you need to update test data, then you good!</p>
|
|
<p>In simple case, your func only have 2 or 3 inputs so table drive tests is still looking good.
|
|
But real world is ugly (maybe not, idk I'm just too young in this industry). Your func can have 5 or 10 inputs, also your func call many third party services.</p>
|
|
<p>Imagine having below func to upload image:</p>
|
|
<div class="highlight highlight-source-go"><pre><span class="pl-k">type</span> <span class="pl-smi">service</span> <span class="pl-k">struct</span> {
|
|
<span class="pl-c1">db</span> <span class="pl-smi">DB</span>
|
|
<span class="pl-c1">redis</span> <span class="pl-smi">Redis</span>
|
|
<span class="pl-c1">minio</span> <span class="pl-smi">MinIO</span>
|
|
<span class="pl-c1">logService</span> <span class="pl-smi">LogService</span>
|
|
<span class="pl-c1">verifyService</span> <span class="pl-smi">VerifyService</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">s</span> <span class="pl-c1">*</span><span class="pl-smi">service</span>) <span class="pl-en">Upload</span>(<span class="pl-s1">ctx</span> context.<span class="pl-smi">Context</span>, <span class="pl-s1">req</span> <span class="pl-smi">Request</span>) <span class="pl-smi">error</span> {
|
|
<span class="pl-c">// I simplify by omitting the response, only care error for now</span>
|
|
<span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">verifyService</span>.<span class="pl-en">Verify</span>(<span class="pl-s1">req</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">err</span>
|
|
}
|
|
|
|
<span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">minio</span>.<span class="pl-en">Put</span>(<span class="pl-s1">req</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">err</span>
|
|
}
|
|
|
|
<span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">redis</span>.<span class="pl-en">Set</span>(<span class="pl-s1">req</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">err</span>
|
|
}
|
|
|
|
<span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">db</span>.<span class="pl-en">Save</span>(<span class="pl-s1">req</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">err</span>
|
|
}
|
|
|
|
<span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">logService</span>.<span class="pl-en">Save</span>(<span class="pl-s1">req</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">err</span>
|
|
}
|
|
|
|
<span class="pl-k">return</span> <span class="pl-c1">nil</span>
|
|
}</pre></div>
|
|
<p>With table driven test and thanks to <a href="https://github.com/stretchr/testify">stretchr/testify</a>, I usually write like this:</p>
|
|
<div class="highlight highlight-source-go"><pre><span class="pl-k">type</span> <span class="pl-smi">ServiceSuite</span> <span class="pl-k">struct</span> {
|
|
suite.<span class="pl-smi">Suite</span>
|
|
|
|
<span class="pl-c1">db</span> <span class="pl-smi">DBMock</span>
|
|
<span class="pl-c1">redis</span> <span class="pl-smi">RedisMock</span>
|
|
<span class="pl-c1">minio</span> <span class="pl-smi">MinIOMock</span>
|
|
<span class="pl-c1">logService</span> <span class="pl-smi">LogServiceMock</span>
|
|
<span class="pl-c1">verifyService</span> <span class="pl-smi">VerifyServiceMock</span>
|
|
|
|
<span class="pl-c1">s</span> <span class="pl-smi">service</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">s</span> <span class="pl-c1">*</span><span class="pl-smi">ServiceSuite</span>) <span class="pl-en">SetupTest</span>() {
|
|
<span class="pl-c">// Init mock</span>
|
|
<span class="pl-c">// Init service</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">s</span> <span class="pl-c1">*</span><span class="pl-smi">ServiceSuite</span>) <span class="pl-en">TestUpload</span>() {
|
|
<span class="pl-s1">tests</span> <span class="pl-c1">:=</span> []<span class="pl-k">struct</span>{
|
|
<span class="pl-c1">name</span> <span class="pl-smi">string</span>
|
|
<span class="pl-c1">req</span> <span class="pl-smi">Request</span>
|
|
<span class="pl-c1">verifyErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">minioErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">redisErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">dbErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">logErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">wantErr</span> <span class="pl-smi">error</span>
|
|
}{
|
|
{
|
|
<span class="pl-c">// Init test case</span>
|
|
}
|
|
}
|
|
|
|
<span class="pl-k">for</span> <span class="pl-s1">_</span>, <span class="pl-s1">tc</span> <span class="pl-c1">:=</span> <span class="pl-k">range</span> <span class="pl-s1">tests</span> {
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Run</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">name</span>, <span class="pl-k">func</span>(){
|
|
<span class="pl-c">// Mock all error depends on test case</span>
|
|
<span class="pl-k">if</span> <span class="pl-s1">tc</span>.<span class="pl-c1">verifyErr</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-s1">s</span>.<span class="pl-c1">verifyService</span>.<span class="pl-en">MockVerify</span>().<span class="pl-en">Return</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">verifyErr</span>)
|
|
}
|
|
<span class="pl-c">// ...</span>
|
|
|
|
<span class="pl-s1">gotErr</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">service</span>.<span class="pl-en">Upload</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">req</span>)
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Equal</span>(<span class="pl-s1">wantErr</span>, <span class="pl-s1">gotErr</span>)
|
|
})
|
|
}
|
|
}</pre></div>
|
|
<p>Looks good right?
|
|
Be careful with this.
|
|
It can go from 0 to 100 ugly real quick.</p>
|
|
<p>What if req is a struct with many fields?
|
|
So in each test case you need to set up req.
|
|
They are almost the same, but with some error case you must alter req.
|
|
It's easy to be init with wrong value here (typing maybe ?).
|
|
Also all req looks similiar, kinda duplicated.</p>
|
|
<div class="highlight highlight-source-go"><pre><span class="pl-s1">tests</span> <span class="pl-c1">:=</span> []<span class="pl-k">struct</span>{
|
|
<span class="pl-c1">name</span> <span class="pl-smi">string</span>
|
|
<span class="pl-c1">req</span> <span class="pl-smi">Request</span>
|
|
<span class="pl-c1">verifyErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">minioErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">redisErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">dbErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">logErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">wantErr</span> <span class="pl-smi">error</span>
|
|
}{
|
|
{
|
|
<span class="pl-c1">req</span>: <span class="pl-smi">Request</span> {
|
|
<span class="pl-c1">a</span>: <span class="pl-s">"a"</span>,
|
|
<span class="pl-c1">b</span>: {
|
|
<span class="pl-c1">c</span>: <span class="pl-s">"c"</span>,
|
|
<span class="pl-c1">d</span>: {
|
|
<span class="pl-s">"e"</span>: <span class="pl-s1">e</span>
|
|
}
|
|
}
|
|
}
|
|
<span class="pl-c">// Other fieles</span>
|
|
},
|
|
{
|
|
<span class="pl-c1">req</span>: <span class="pl-smi">Request</span> {
|
|
<span class="pl-c1">a</span>: <span class="pl-s">"a"</span>,
|
|
<span class="pl-c1">b</span>: {
|
|
<span class="pl-c1">c</span>: <span class="pl-s">"c"</span>,
|
|
<span class="pl-c1">d</span>: {
|
|
<span class="pl-s">"e"</span>: <span class="pl-s1">e</span>
|
|
}
|
|
}
|
|
}
|
|
<span class="pl-c">// Other fieles</span>
|
|
},
|
|
{
|
|
<span class="pl-c1">req</span>: <span class="pl-smi">Request</span> {
|
|
<span class="pl-c1">a</span>: <span class="pl-s">"a"</span>,
|
|
<span class="pl-c1">b</span>: {
|
|
<span class="pl-c1">c</span>: <span class="pl-s">"c"</span>,
|
|
<span class="pl-c1">d</span>: {
|
|
<span class="pl-s">"e"</span>: <span class="pl-s1">e</span>
|
|
}
|
|
}
|
|
}
|
|
<span class="pl-c">// Other fieles</span>
|
|
}
|
|
}</pre></div>
|
|
<p>What if dependencies of service keep growing?
|
|
More mock error to test data of course.</p>
|
|
<div class="highlight highlight-source-go"><pre> <span class="pl-s1">tests</span> <span class="pl-c1">:=</span> []<span class="pl-k">struct</span>{
|
|
<span class="pl-c1">name</span> <span class="pl-smi">string</span>
|
|
<span class="pl-c1">req</span> <span class="pl-smi">Request</span>
|
|
<span class="pl-c1">verifyErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">minioErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">redisErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">dbErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">logErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">wantErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c">// Murr error</span>
|
|
<span class="pl-c1">aErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">bErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c1">cErr</span> <span class="pl-smi">error</span>
|
|
<span class="pl-c">// ...</span>
|
|
}{
|
|
{
|
|
<span class="pl-c">// Init test case</span>
|
|
}
|
|
}</pre></div>
|
|
<p>The test file keep growing longer and longer until I feel sick about it.</p>
|
|
<p>See <a href="https://github.com/tektoncd/pipeline/blob/main/pkg/pod/pod_test.go">tektoncd/pipeline unit test</a> to get a feeling about this.
|
|
When I see it, <code>TestPodBuild</code> has almost 2000 lines.</p>
|
|
<p>The solution I propose here is simple (absolutely not perfect, but good with my usecase) thanks to <strong>stretchr/testify</strong>.
|
|
I init all <strong>default</strong> action on <strong>success</strong> case.
|
|
Then I <strong>alter</strong> request or mock error for unit test to hit on other case.
|
|
Remember if unit test is hit, code coverate is surely increaesed, and that my <strong>goal</strong>.</p>
|
|
<div class="highlight highlight-source-go"><pre><span class="pl-c">// Init ServiceSuite as above</span>
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">s</span> <span class="pl-c1">*</span><span class="pl-smi">ServiceSuite</span>) <span class="pl-en">TestUpload</span>() {
|
|
<span class="pl-c">// Init success request</span>
|
|
<span class="pl-s1">req</span> <span class="pl-c1">:=</span> <span class="pl-smi">Request</span>{
|
|
<span class="pl-c">// ...</span>
|
|
}
|
|
|
|
<span class="pl-c">// Init success action</span>
|
|
<span class="pl-s1">s</span>.<span class="pl-c1">verifyService</span>.<span class="pl-en">MockVerify</span>().<span class="pl-en">Return</span>(<span class="pl-c1">nil</span>)
|
|
<span class="pl-c">// ...</span>
|
|
|
|
<span class="pl-s1">gotErr</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">service</span>.<span class="pl-en">Upload</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">req</span>)
|
|
<span class="pl-s1">s</span>.<span class="pl-en">NoError</span>(<span class="pl-s1">gotErr</span>)
|
|
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Run</span>(<span class="pl-s">"failed"</span>, <span class="pl-k">func</span>(){
|
|
<span class="pl-c">// Alter failed request from default</span>
|
|
<span class="pl-s1">req</span> <span class="pl-c1">:=</span> <span class="pl-smi">Request</span>{
|
|
<span class="pl-c">// ...</span>
|
|
}
|
|
|
|
<span class="pl-s1">gotErr</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">service</span>.<span class="pl-en">Upload</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">req</span>)
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Error</span>(<span class="pl-s1">gotErr</span>)
|
|
})
|
|
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Run</span>(<span class="pl-s">"another failed"</span>, <span class="pl-k">func</span>(){
|
|
<span class="pl-c">// Alter verify return</span>
|
|
<span class="pl-s1">s</span>.<span class="pl-c1">verifyService</span>.<span class="pl-en">MockVerify</span>().<span class="pl-en">Return</span>(<span class="pl-s1">someErr</span>)
|
|
|
|
|
|
<span class="pl-s1">gotErr</span> <span class="pl-c1">:=</span> <span class="pl-s1">s</span>.<span class="pl-c1">service</span>.<span class="pl-en">Upload</span>(<span class="pl-s1">tc</span>.<span class="pl-c1">req</span>)
|
|
<span class="pl-s1">s</span>.<span class="pl-en">Error</span>(<span class="pl-s1">gotErr</span>)
|
|
})
|
|
|
|
<span class="pl-c">// ...</span>
|
|
}</pre></div>
|
|
<p>If you think this is not quick enough, just <strong>ignore</strong> the response.
|
|
You only need to check error or not if you want code coverage only.</p>
|
|
<p>So if request change fields or more dependencies, I need to update success case, and maybe add corresponding error case if need.</p>
|
|
<p>Same idea but still with table, you can find here <a href="https://arslan.io/2022/12/04/functional-table-driven-tests-in-go/" rel="nofollow">Functional table-driven tests in Go - Fatih Arslan</a>.</p>
|
|
|
|
<a href="mailto:hauvipapro+posts@gmail.com"
|
|
>Feel free to ask me via email</a
|
|
>
|
|
<a rel="me" href="https://hachyderm.io/@haunguyen">Mastodon</a>
|
|
</body>
|
|
</html>
|