Testing web applications, context objects, and DI

xuanbao · · 378 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>I&#39;m really quite new to Go so am still trying to grasp some of the fundamentals of what makes idiomatic code. I&#39;ve seen some great posts about best-practices and that sort of thing, and tools to use to ensure that those best practices are being followed; but one thing I&#39;m still quite confused about is how to effectively and idiomatically test a web application.</p> <p>Specifically here I&#39;m talking about handler functions. I&#39;ve seen a lot of these &#34;context&#34; instances floating around in various projects that seem to be some kind of semi-global box. I&#39;ve also seen what is effectively global static instances of things. Neither of these things really appeal to me as both seem to either disregard compile-time type safety, or the ability to test things by mocking / switching implementations.</p> <p>One interesting thing I did see quite recently is the idea of making a function that would return a handler function with the dependencies it needs passed passed to the function that returns the handler, something like:</p> <pre><code>func homeHandler(db Database) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, &#34;Hello, %s&#34;, db.GetName()) }) } </code></pre> <p><em>(Assuming that <code>Database</code> is an interface here, so you could pass in the &#34;real&#34; database, or a mock implementation that would allow you to test the handler)</em></p> <p>This approach does appeal to me, but I&#39;ve been trying to look around as many projects as I can that seem popular to find examples, yet I&#39;ve not come across this pattern in any of them yet. Is there something wrong with it that I&#39;m missing? Is this something that actually relatively common?</p> <p>Edit: Being downvoted for asking a question? I&#39;m trying to learn here! :/</p> <hr/>**评论:**<br/><br/>bcokert: <pre><p>One of the common problems I encounter when testing web applications, after learning about the utility of interfaces, is that many popular frameworks and setups don&#39;t implement their own interfaces - instead they write giant structs with many pointer receiver methods. This is very nice for the actual implementation, but makes testing harder. A common solution to this is to inject your own interfaces to your methods/functions, and compose them of existing interfaces when necessary. For example, the Gin-gonic/gin library has handlers that expect a pointer to a Context, which is a struct rather than an interface. Instead of writing your handlers to accept a Context, make them accept an interface, one containing the methods of the context that you&#39;ll be using. For the real server, the context will implement the interface, and for your tests, you&#39;d create a mock context that implements that interface. If you use the data on the context, and not just the methods, you simply add that data to the mock. This is just one example, but I find that it embodies the general pattern: make your methods accept interfaces, and it makes them easier to test. For integration tests, take a look at http test.RespinseRecorder, and the other parts of that package. Good luck!</p></pre>SeerUD: <pre><p>Cool, that sounds like some solid advice - I&#39;ll definitely keep it in mind. I&#39;m still getting used to things like duck typing coming from PHP, Java, and Scala.</p></pre>interactiv_: <pre><p>So basically <a href="/u/bcokert" rel="nofollow">u/bcokert</a> says use a framework,whether it is your own or someone else&#39;s. Because that&#39;s what Gin is. You cannot stick to the default HandleFunc signature if you want to do something a bit complex. It will lead to an untestable mess.</p> <p>HandleFunc or HttpHandler interface should be used AT THE TOP LEVEL of your framework. But what goes inside your framework frankly, it does need to be compatible with the that interface.</p> <p>So again , in simple basic cases , yes using closures like you did in your exemple is acceptable.</p> <p>But when you&#39;re getting an API with 100 routes, it is not. You need to throw away the (responsewriter, *request) signature to avoid an avalanche of structs, closures and globals.</p></pre>TheMerovius: <pre><p>What I usually do is, to a) have a context for request-scoped data (e.g. session and transaction) and don&#39;t care about compile-time type-safety. It&#39;s usually not a huge problem and very simple to test, so who really cares, in practice? And b) make a struct that contains all the state the server needs to hold and then use that to mock out logger and databases in tests. As long as you don&#39;t actually use any global variables, that&#39;s very simple to test because you get whatever level of isolation you wish in your tests.</p> <p>So I would use the closure-pattern for request-scoped data (but pass a context around where the state is actually exposed) and the struct-pattern for all state that needs to survive requests (and try to minimize that. Stateless HTTP-Servers are much better in any regard).</p></pre>SeerUD: <pre><p>Thanks for writing this out. I have one question about the last point; would a database connection be considered to be state that needs to survive requests? I was thinking that a database connection could be kept open in a long-running process, instead of opening a new connection per-request?</p></pre>TheMerovius: <pre><p>Yes, I don&#39;t count &#34;connections to other services&#34; as real state. You <em>could</em> reconnect on every request or when your process/server/harddisk/whatever dies, so it&#39;s merely a performance-optimization to have one connection overall, it has no effect on correctness. So my habbit is to put a long-living connection into the struct and have a per-request transaction which I save in the context.</p></pre>SeerUD: <pre><p>Cool, that makes a lot of sense. Thanks again!</p></pre>junajted: <pre><p>i organize handlers in controllers. controllers are structs which accept dependencies (like db Database). handlers are methods on controller receivers</p> <p>i recommend this article which explains different approaches to organizing db access, if you haven&#39;t came across. <a href="http://www.alexedwards.net/blog/organising-database-access" rel="nofollow">http://www.alexedwards.net/blog/organising-database-access</a></p></pre>SeerUD: <pre><p>This is probably the way that would make the most sense to me, it sounds similar to the &#34;env struct&#34; pattern I&#39;ve seen a few places, including in other comments here. I think you could probably take this a bit further and make it a bit more modular by treating the structs you assign dependencies to as if they were controllers.</p> <p>Cheers!</p></pre>oh-thatguy: <pre><p>This is the approach I&#39;ve been using. It allows the controllers to be independently tested, and makes it very explicit which dependencies the controller requires. The only thing I&#39;ve struggled with is whether to make the controllers submodules, or organize into the top level module.</p></pre>interactiv_: <pre><p>Not surprised you&#39;re being downvoted by some. Go standard library is obviously designed badly on this precise matter, but the Go community refuses to see it. </p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

378 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传