
{"id":5290,"date":"2025-07-30T00:12:46","date_gmt":"2025-07-30T04:12:46","guid":{"rendered":"https:\/\/ikriv.com\/blog\/?p=5290"},"modified":"2025-07-30T00:12:46","modified_gmt":"2025-07-30T04:12:46","slug":"writing-unit-tests-with-cursor-ide","status":"publish","type":"post","link":"https:\/\/ikriv.com\/blog\/?p=5290","title":{"rendered":"Writing unit tests with Cursor IDE"},"content":{"rendered":"<p>I created a &#8220;view model&#8221; like hook in React, and asked Cursor to write unit tests for it.<br \/>\nThe hook does not have any HTML code, but it uses a bunch of React calls like <code>useState<\/code>, <code>useContext<\/code>, and the like.<br \/>\nSomething like the one below, but more complicated.<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\nimport { useState, useCallback } from &#039;react&#039;;\r\n\r\nexport function useCounterViewModel(initialValue: number = 0) {\r\n  const &#x5B;count, setCount] = useState(initialValue);\r\n\r\n  const increment = useCallback(() =&gt; setCount((c) =&gt; c + 1), &#x5B;]);\r\n  const decrement = useCallback(() =&gt; setCount((c) =&gt; c - 1), &#x5B;]);\r\n  const reset = useCallback(() =&gt; setCount(initialValue), &#x5B;initialValue]);\r\n\r\n  \/\/ Derived visual state\r\n  const isPositive = count &gt; 0;\r\n  const isNegative = count &lt; 0;\r\n  const formatted = `Current count: ${count}`;\r\n\r\n  return {\r\n    count,\r\n    formatted,\r\n    isPositive,\r\n    isNegative,\r\n    increment,\r\n    decrement,\r\n    reset,\r\n  };\r\n}\r\n<\/pre>\n<p><b>Good stuff<\/b>: Cursor was able to mock everything that needed to be mocked. It would have probably taken me a few hours to setup all the mocks correctly. There are ~100 lines of mocking code for all kinds of things React.<\/p>\n<p><b>Bad stuff<\/b>: the tests Cursor initially wrote were full of long repetitive statements. Cursor is pretty bad at making the tests short and does not seem to believe in &#8220;do not repeat yourself&#8221;. It may have worked better if I explicitly instructed it to avoid repetition, but I didn&#8217;t.<\/p>\n<p>it(&quot;test1&quot;, () =&gt; {<br \/>\n   const mockData = {&#8230; long initializer&#8230;};<br \/>\n   buildState.currentElement.draft = mockData;<br \/>\n   const { result } = renderHook();<br \/>\n   expect(result.current?&#8230;).toBe(..);<br \/>\n});<\/p>\n<p>it(&quot;test2&quot;, () =&gt; {<br \/>\n   const mockData = {&#8230; same long initializer&#8230;};<br \/>\n   buildState.currentElement.draft = mockData;<br \/>\n   const { result } = renderHook();<br \/>\n   expect(result.current?&#8230;).toBe(..);<br \/>\n});<\/p>\n<p>It took about an hour to bring it to a half-decent shape, where each test does not take 50 lines of repetitive code. Cursor (as other AI models) is too slow when it comes to mass-editing the code. Of course, there is always find-and-replace, which is instantaneous, but if you need something just a notch above dumb find and replace, you are stuck. Cursor initially generated a ~500 lines of code file, and every refactoring took several minutes.<\/p>\n<p><b>Ugly stuff<\/b><br \/>\nSome tests didn&#8217;t really test anything. In other words, the did not contain the &#8220;expect&#8221; statement. <\/p>\n<p>In one other case, Cursor created a test that relied on side effects of the previous test. Like, &#8220;start from nothing, add a field, make sure you now have two fields&#8221;. Why two fields? Because previous test added another field, and we never cleaned it up \ud83d\ude42<\/p>\n<p>So, the test code generated by Cursor was mediocre at best, but I am really grateful for the mocks.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I created a &#8220;view model&#8221; like hook in React, and asked Cursor to write unit tests for it. The hook does not have any HTML code, but it uses a <a href=\"https:\/\/ikriv.com\/blog\/?p=5290\" class=\"more-link\">[&hellip;]<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"Layout":"","footnotes":""},"categories":[4,29],"tags":[],"class_list":["entry","author-ikriv","post-5290","post","type-post","status-publish","format-standard","category-hack","category-typescript"],"_links":{"self":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5290","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5290"}],"version-history":[{"count":1,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5290\/revisions"}],"predecessor-version":[{"id":5291,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5290\/revisions\/5291"}],"wp:attachment":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5290"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5290"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5290"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}