365 lines
18 KiB
HTML
365 lines
18 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.2.0/github-markdown.min.css"
|
|
/>
|
|
<title>haunt98 posts</title>
|
|
</head>
|
|
<style>
|
|
.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">
|
|
<div><a href="index.html">Index</a></div>
|
|
<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 coverage is surely increased, 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>
|
|
|
|
<div>
|
|
Feel free to ask me via
|
|
<a href="mailto:hauvipapro+posts@gmail.com">email</a> or
|
|
<a rel="me" href="https://hachyderm.io/@haunguyen">Mastodon</a>. Source
|
|
code is available on
|
|
<a href="https://github.com/haunt98/posts-go">GitHub</a>
|
|
</div>
|
|
</body>
|
|
</html>
|