{"id":20795,"date":"2017-10-04T15:09:56","date_gmt":"2017-10-04T15:09:56","guid":{"rendered":"https:\/\/legacy.livecode.com\/?p=20795"},"modified":"2017-10-11T09:28:17","modified_gmt":"2017-10-11T09:28:17","slug":"testing-1-2-28534","status":"publish","type":"post","link":"https:\/\/legacy.livecode.com\/testing-1-2-28534\/","title":{"rendered":"Testing 1, 2, 28534"},"content":{"rendered":"<p>I really enjoy writing tests. It&#8217;s great to ensure that a new piece of functionality will never be broken, or that a bug will never be reintroduced. It&#8217;s doubly satisfying when something previously untestable becomes testable, when a certain level of struggle or ingenuity is required to work out <em>how<\/em>\u00a0to test something, or when a test is particularly (perhaps even maximally) succinct.<!--more--><\/p>\n<p>I really enjoy writing tests. It&#8217;s great to ensure that a new piece of functionality will never be broken, or that a bug will never be reintroduced. It&#8217;s doubly satisfying when something previously untestable becomes testable, when a certain level of struggle or ingenuity is required to work out\u00a0<em>how<\/em>\u00a0to test something, or when a test is particularly (perhaps even maximally) succinct.<\/p>\n<p>LiveCode&#8217;s continuous integration had its 2nd birthday recently, and ever since a &#8216;quick&#8217; test runner was implemented for this purpose, the number of tests and the test infrastructure itself has ballooned.<\/p>\n<p>To celebrate this birthday, in this blog post I&#8217;ll be describing all the different sorts of test we run, and picking out a few of my favourite examples.<\/p>\n<h2>Engine tests<\/h2>\n<p>The engine test runner uses the standalone engine to execute each test handler defined in the test scripts in its own subprocess. These tests &#8211; at least those in the &#8216;core&#8217; section &#8211; are intended to ensure that every single variant of syntax, each of which should have received a distinct code-path in the syntax refactoring project, has a test of its functionality. Ideally, every execution and parse error would also have a test.<\/p>\n<p>For example, a simple engine test might be<\/p>\n<pre><code>\r\non TestCreateStack\r\n    create stack \"test\"\r\n    TestAssert \"Stack created\", there is a stack \"test\"\r\nend TestCreateStack\r\n<\/code><\/pre>\n<p>The <code>TestAssert<\/code> handler takes two arguments,<\/p>\n<pre><code>\r\nTestAssert pDescription, pExpectTrue\r\n<\/code><\/pre>\n<p>If the expression passed in <code>pExpectTrue<\/code> is true, the test with description <code>pDescription<\/code> passes. If not, it fails.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/core\/engine\/behavior.livecodescript#L23\">Cyclic behavior error<\/a><\/h3>\n<pre><code>\r\non _TestCyclicBehavior pStack\r\n    set the behavior of (the behavior of pStack) to pStack\r\nend _TestCyclicBehavior\r\n\r\non TestCyclicBehavior\r\n    create stack \"Behavior\"\r\n    create stack\r\n    set the behavior of it to the long id of stack \"Behavior\"\r\n    TestAssertThrow \"cycle in behavior script hierarchy throws\", \\\r\n        \"_TestCyclicBehavior\", the long id of me, \\\r\n        \"EE_PARENTSCRIPT_CYCLICOBJECT\", it\r\nend TestCyclicBehavior\r\n<\/code><\/pre>\n<h4>What does it test?<\/h4>\n<p>This test ensures that it is an error if there is a cycle in the behavior hierarchy.<\/p>\n<h4>How does it test?<\/h4>\n<p>The <code>TestAssertThrow<\/code> handler is a test utility that allows you to test that the expected error is thrown when a given handler is executed. The <code>TestAssertThrow<\/code> handler has parameters as follows:<\/p>\n<pre><code>\r\nTestAssertThrow pDescription, pHandlerName, pTarget, pExpectedError, pParam\r\n<\/code><\/pre>\n<p>Its function could be described as: execute handler <code>pHandlerName<\/code> which is implemented in the script of <code>pTarget<\/code>, (with an optional parameter <code>pParam<\/code>); pass the test with description <code>pDescription<\/code> if the error with code <code>pExpectedError<\/code> is thrown as a result.<\/p>\n<p>The error in this case is &#8220;parentScript: loop in hierarchy&#8221;, which you can find by checking the <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/engine\/src\/executionerrors.h\">execution errors source<\/a>.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>I must admit, my enjoyment of this test is almost exclusively bound to the simplicity of the error-throwing line of code. It&#8217;s neat.<\/p>\n<h4>Why is it important?<\/h4>\n<p>This needs to be an error because otherwise there would be an infinite loop when trying to find a handler in one of the objects&#8217; message paths.<\/p>\n<h4>Other good tests:<\/h4>\n<ul>\n<li><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/core\/chunks\/char.livecodescript#L49\">The rather complex-looking char tests<\/a> which mirror the <a href=\"http:\/\/unicode.org\/reports\/tr29\/#Grapheme_Cluster_Boundary_Rules\">Unicode grapheme cluster boundary rules<\/a><\/li>\n<li>More <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/core\/logic\/infinity.livecodescript\">test cases that \u221e is \u221e<\/a> than you probably thought possible and\/or necessary<\/li>\n<\/ul>\n<h2>IDE tests<\/h2>\n<p>In stark contrast to the engine tests, the IDE tests are not at all self-contained. They test things like the high-level IDE API, and functionality of stacks within the IDE.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode-ide\/blob\/32d03e9add04146e8c48bd7950c030c3b3fc06f6\/tests\/standalonebuilder\/inclusion.livecodescript\">revXML (and other external) inclusions<\/a><\/h3>\n<h4>What does it test?<\/h4>\n<p>This set of tests ensures that apps built with standalone builder can load the externals that are selected (or detected) for inclusion.<\/p>\n<h4>How does it test?<\/h4>\n<p>The core of this test is creating a standalone application with the following script:<\/p>\n<pre><code>\r\non testInclusion\r\n    get revXMLTrees()\r\n    if the result is empty then\r\n        quit 0\r\n    else\r\n        write the result to stderr\r\n        quit 1\r\n    end if\r\nend testInclusion\r\n\r\non startup\r\n    try\r\n        testInclusion\r\n    catch tError\r\n        write \"inclusion not loaded\" to stderr\r\n        quit 1\r\n    end try\r\nend startup\r\n<\/code><\/pre>\n<p>The &#8216;search for inclusions&#8217; feature should see the XML-related function in the stack script when building the standalone and include the revXML external accordingly. Then the test executes the newly built standalone and checks that the exit code is 0. Quitting with exit code 1 if the result is not empty ensures that if revXML external had internal errors when calling <code>revXMLTrees<\/code>, the test fails. Similarly, the try loop ensures that if <code>revXMLTrees<\/code> doesn&#8217;t exist (because the revXML external is missing), the standalone quits with exit code 1.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>I enjoy the fact that the standalone engine is running the development engine as a subprocess, which in turn runs the standalone engine as a subprocess. However this is a quirk of how the IDE tests are run, so I&#8217;ll also say I like it for the same reason it&#8217;s so important.<\/p>\n<h4>Why is it important?<\/h4>\n<p>Loading dynamic libraries can be tricky, especially where multiple platforms are involved.<\/p>\n<ul>\n<li>If multiple versions of a library are present on a system, various things like environment variables can affect which version is found and loaded.<\/li>\n<li>On Mac, both shared libraries and bundles can be loaded dynamically, but in a slightly different way<\/li>\n<li>Sometimes they can have special requirements (such as location or existence of other libraries or code resources)<\/li>\n<\/ul>\n<h4>Other good tests:<\/h4>\n<ul>\n<li>An ever-growing set of <a href=\"https:\/\/github.com\/livecode\/livecode-ide\/blob\/develop\/tests\/messagebox\/execution.livecodescript\">tests for the intricate message box autocomplete<\/a> features<\/li>\n<li><a href=\"https:\/\/github.com\/livecode\/livecode-ide\/blob\/develop\/tests\/core\/ideelements\/colortexts.livecodescript\">Tests for the correspondence between color names and RGB values<\/a> in the IDE<\/li>\n<\/ul>\n<h2>LCB Standard Library Tests<\/h2>\n<p>The core LCB tests are essentially to LiveCode Builder as the engine tests are to LiveCode Script. They use the lc-run program, which can execute LCB bytecode files directly, to run the tests.<\/p>\n<p><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcb\/stdlib\/list.lcb\">List tests<\/a><\/p>\n<p>Eg &#8216;the number of elements in&#8217; test:<\/p>\n<pre><code>\r\npublic handler TestCount()\r\n    test \"count\" when the number of elements in [1, 2, [\"a\", \"b\"]] is 3\r\nend handler\r\n<\/code><\/pre>\n<p>I didn&#8217;t want to pick out an individual test here, just wanted to highlight the List tests in their entirety. TestCount is included as an illustration of the test syntax in LCB.<\/p>\n<h4>What do they test?<\/h4>\n<p>The clue is in the name &#8211; they test various aspects of the syntax associated with Lists.<\/p>\n<h4>How do they test?<\/h4>\n<p>Most of these tests create a specific list, perform an operation on them and use the LCB test syntax to verify the expected result.<\/p>\n<h4>Why do I like them?<\/h4>\n<p>I like these tests because I like the LCB List type very much. There are several aspects of string \/ delimiter based lists that LiveCode Script uses which can be confusing, or appear anomalous, or actually are anomalous. The List type is free of these complications, and more efficient than a string list under the hood.<\/p>\n<h4>Why is it important?<\/h4>\n<p>The List type is used in almost every widget, library and support module that has so far been implemented!<\/p>\n<h2>LCB Virtual Machine Tests<\/h2>\n<p>These tests ensure that the fundamental parts of LCB (i.e. the control structures, function calling, etc) which are essentially hard-coded in LCB (as opposed to the modular, plugged-in standard library) work correctly.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcb\/vm\/dynamic-call.lcb#L5\">Thunks<\/a><\/h3>\n<pre><code>\r\nhandler type Thunk()\r\n\r\nhandler Yes()\r\n    return true\r\nend handler\r\n\r\nhandler InvokeArgument(in pFunc as Thunk)\r\n    return pFunc()\r\nend handler\r\n\r\npublic handler TestDynamicInvokeVariable()\r\n    variable pFunc as Thunk\r\n    put Yes into pFunc\r\n    test \"dynamic invoke (variable)\" when pFunc()\r\nend handler\r\n\r\npublic handler TestDynamicInvokeArgument()\r\n    test \"dynamic invoke (argument)\" when InvokeArgument(Yes)\r\nend handler\r\n<\/code><\/pre>\n<h4>What does it test?<\/h4>\n<p>Invocation of handlers that are stored in variables or passed as arguments to functions.<\/p>\n<h4>How does it test?<\/h4>\n<p>Handler types can be declared in LCB, and handlers which conform to the type can be put into variables of that type. A handler conforms to a handler type if its parameter and return value types match.<\/p>\n<p>In this case, the handler type <code>Thunk<\/code> is defined with no parameters and the default return type (optional any), so the <code>Yes<\/code> handler obviously conforms to the handler type. The <code>Yes<\/code> handler is passed directly to <code>InvokeArgument<\/code>, and also put into a variable and passed to <code>InvokeArgument<\/code>.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>Partly because the word &#8216;Thunk&#8217; is great.<\/p>\n<h4>Why is it important?<\/h4>\n<p>The ability to pass handlers as arguments is one of the most powerful features of LiveCode Builder. One of the most common use cases is for recursively performing some kind of action on an array &#8211; there are several examples of this in the Tree View widget.<\/p>\n<h2>LCB Compiler Tests<\/h2>\n<p>LCB Compiler tests ensure that the lc-compile program, which parses LCB modules, either succeeds when it&#8217;s supposed to, or outputs the correct error message, indicating the correct part of the offending line of code.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcb\/compiler\/frontend\/line-continuation.compilertest\">Line Continuation tests<\/a><\/h3>\n<pre>%TEST ContinuationPreComment\r\nmodule %{CONTINUATION_BEFORE_COMMENT} \\ -- test\r\n    compiler_test\r\nend module\r\n%EXPECT PASS\r\n%ERROR \"Illegal token ''\" AT CONTINUATION_BEFORE_COMMENT\r\n%ENDTEST\r\n<\/pre>\n<h4>What does it test?<\/h4>\n<p>This test ensures that a line continuation character consumes a following newline, and backslash followed by a comment isn&#8217;t a line continuation.<\/p>\n<h4>How does it test?<\/h4>\n<p>The <code>%{MARKER}<\/code> syntax is used in compiler tests to specify the expected location of a syntax error. So this test simply consists of a module script which would only succeed in compiling if &#8211;test were considered a line continuation.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>I like this test because we believe this is the correct and expected behavior when putting a comment after a line continuation character, but discovered that such a construction has always been valid in LiveCode Script. It&#8217;s a neat illustration of the situation.<\/p>\n<h4>Why is it important?<\/h4>\n<p>Where a decision is made to depart from some of the anomalies of LiveCode Script, it&#8217;s doubly important that tests exist for the situation at hand in order to indicate that the departure is not accidental.<\/p>\n<h4>Other good tests:<\/h4>\n<ul>\n<li>Tests that ensure the <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcb\/compiler\/frontend\/namespace.compilertest\">LCB namespace operator &#8216;.&#8217; works as expected<\/a><\/li>\n<\/ul>\n<h2>LCS Parser Tests<\/h2>\n<p>LCS Parser tests ensure that LiveCode Script snippets which should compile without error do so, and also that correct error messages are thrown for particular syntax errors.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecodeali\/livecode\/blob\/55a526f0cd7bf1964b2abffbf90923fcaf5d01a3\/tests\/lcs\/parser\/properties.parsertest\">Object property parsing test<\/a><\/h3>\n<pre>%TEST GetTheObjectPropertyNoObject\r\non parse_test\r\n get the width %{AFTER_PROPERTY}\r\nend parse_test\r\n%EXPECT PASS\r\n%ERROR PE_PROPERTY_NOTOF AT AFTER_PROPERTY\r\n%ENDTEST\r\n<\/pre>\n<h4>What does it test?<\/h4>\n<p>This test ensures that properties which must have an object target cause a parser error when the object target is missing.<\/p>\n<h4>How does it test?<\/h4>\n<p>By ensuring that PE_PROPERTY_NOTOF is thrown for the line <code>get the width<\/code>, as width is not a global property.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>I like this test because\u00a0it&#8217;s so much neater than <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop-8.1\/tests\/lcs\/parser\/parser.livecodescript\">the corresponding test in develop-8.1<\/a>\u00a0where the parser test runner does not exist.<\/p>\n<h4>Why is it important?<\/h4>\n<p>The message box needs to know whether a property is global or not in order to\u00a0do its intelligence object autocompletion. With this parser error in place, the message box knows not to execute <code>get the width<\/code>\u00a0by itself, but instead autocompletes to <code>get the width of stack \"Untitled 1\"<\/code>\u00a0or whatever the currently chose target object is.<\/p>\n<h4>Other good tests:<\/h4>\n<ul>\n<li><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/parser\/line-continuation.parsertest\">Line continuation tests<\/a>\u00a0which are the LCS counterpart to the LCB compiler line continuation tests mentioned above<\/li>\n<\/ul>\n<h2>C++ tests<\/h2>\n<p>C++ tests are (unsurprisingly) written in C++, and are only written for things that are too low-level to test any other way. These tests use the Google test framework and live in a subfolder of the main folder of source code in the engine repository.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/engine\/test\/test_lextable.cpp\">Lextable ordering<\/a><\/h3>\n<pre><code>\r\nTEST(lextable, table_pointer)\r\n{\r\n    extern LT *table_pointers[];\r\n    extern const uint4 table_pointers_size;<\/code> \r\n extern uint2 table_sizes[]; \r\n extern const uint4 table_sizes_size; \r\n ASSERT_EQ(table_pointers_size, table_sizes_size); \r\n \r\n for (uint4 i = 0; i &lt; table_pointers_size; i++) \r\n { \r\n LT* table = table_pointers[i]; \r\n const uint4 table_size = table_sizes[i]; \r\n ASSERT_GE(table_size, (unsigned)1); \r\n for (uint4 j = 0; j &lt; table_size - 1; j++) \r\n { \r\n EXPECT_LT(strcmp(table[j].token, table[j+1].token), 0) \r\n &lt;&lt; \"\\\"\" &lt;&lt; table[j+1].token &lt;&lt; \"\\\"\" \r\n &lt;&lt; \" comes before \" &lt;&lt; \"\\\"\" \r\n &lt;&lt; table[j].token &lt;&lt; \"\\\"\"; \r\n }\r\n } \r\n}<\/pre>\n<h4>What does it test?<\/h4>\n<p>That the tokens that make up LiveCode script syntax are listed in alphabetical order.<\/p>\n<h4>How does it test?<\/h4>\n<p>By iterating through the list and ensuring each entry comes before the following entry in the table.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>See &#8216;Why is it important?&#8217;<\/p>\n<h4>Why is it important?<\/h4>\n<ul>\n<li><a href=\"http:\/\/quality.livecode.com\/show_bug.cgi?id=11351\">An annoying bug<\/a><\/li>\n<li><a href=\"http:\/\/quality.livecode.com\/show_bug.cgi?id=15756\">Another annoying bug<\/a><\/li>\n<\/ul>\n<p>Note my irritation at this sort of bug expressed in the latter report.<\/p>\n<p>Syntax tokens are mapped to numeric constants using a binary search of these tables. The best case scenario with a misplaced token is that only the token itself is not found. But if the misplaced token happens to be used in a step of the binary search to decide on the next part of the table to look at, it could cause syntax errors for a whole swathe of tokens.<\/p>\n<h4>Other good tests:<\/h4>\n<p>Well, there are only three more<\/p>\n<ul>\n<li>One ensures that <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/engine\/test\/test_rgb.cpp\">the rgb values are in the correct order<\/a>.<\/li>\n<li>Another ensures the <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/engine\/test\/test_new.cpp\">engine doesn&#8217;t crash when memory allocation via the new operator fails<\/a>.<\/li>\n<li>Finally one ensures that <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/engine\/test\/test_match_name.cpp\">the object type table is complete<\/a> and matches the order of defined object chunk types.<\/li>\n<\/ul>\n<h2>Extension Tests<\/h2>\n<p>Extension tests are written in LCB or LCS depending on the type of extension. There are currently four types of extension:<\/p>\n<ul>\n<li>LCS libraries<\/li>\n<li>Widgets<\/li>\n<li>LCB Libraries<\/li>\n<li>LCB Modules<\/li>\n<\/ul>\n<p>LCS library tests and widget tests are written in LCS, since these both<br \/>\ndepend on the engine for their functionality. LCB modules are not directly accessible through LCS, so their tests are written in LCB. LCB library tests can be written in either LCS or LCB (although currently the latter is restricted by the fact that the library must have no module dependencies)<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/extensions\/libraries\/json\/tests\/JSONTestSuite.livecodescript\">JSON library test suite<\/a><\/h3>\n<h4>What does it test?<\/h4>\n<p>That the LCB JSON library can handle a large range of incoming json data correctly<\/p>\n<h4>How does it test?<\/h4>\n<p>The test loads a series of .json files which are either intended to parse successfully, or fail, and ensures they do just that.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>The sheer extent of the tests and number of edge cases gives this test suite a certain charm.<\/p>\n<h4>Why is it important?<\/h4>\n<p>As JSON data is likely to come from outside sources, it is vitally important that the JsonImport handler agrees with the rest of the world on what is valid or not.<\/p>\n<h4>Other good tests:<\/h4>\n<ul>\n<li>Script items module, <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/extensions\/modules\/scriptitems\/tests\/stringitems.lcb\">string items tests<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/extensions\/script-libraries\/getopt\/tests\/getopt.livecodescript\">Command line option parsing<\/a> script library tests<\/li>\n<\/ul>\n<h2>Docs Tests<\/h2>\n<p>This is not strictly a subcategory, in that the docs tests are run as part of the engine test suite. But it&#8217;s worth mentioning them anyway! At some point they could be run separately.<\/p>\n<h3><a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/docs\/validate-dictionary.livecodescript\">Dictionary validation<\/a><\/h3>\n<h4>What does it test?<\/h4>\n<p>This set of tests ensures that the .lcdoc dictionary files are correctly formatted, that the example scripts compile, and that the syntax specification is correct.<\/p>\n<h4>How does it test?<\/h4>\n<p>This test use the functions defined in the revDocsParser library to obtain the data from the dictionary in the form of a LiveCode array. It then performs a battery of checks on various aspects of that array.<\/p>\n<h4>Why do I like it?<\/h4>\n<p>Because it performs almost 30,000 tests on the docs.<\/p>\n<h4>Why is it important?<\/h4>\n<p>Misleading documentation is bad, for obvious reasons.<\/p>\n<h2>Broken\u00a0Tests<\/h2>\n<p>No, this isn&#8217;t a\u00a0perverse subcategory of tests, but a note on an important feature of the tester API. It is possible to assert that a particular feature is broken. For example, there is currently a <a href=\"http:\/\/quality.livecode.com\/show_bug.cgi?id=16551\">bug<\/a> with the behavior of the <code>hilitedButtonName<\/code>\u00a0property, and there are tests in place which assert the <a href=\"https:\/\/github.com\/livecode\/livecode\/blob\/develop\/tests\/lcs\/core\/interface\/interface-button-props.livecodescript#L35\">broken-ness of that particular test case<\/a>.<\/p>\n<p>The command is<\/p>\n<pre><code>TestAssertBroken &lt;description of test&gt;, &lt;broken test case&gt;, &lt;reason broken&gt;<\/code><\/pre>\n<p>This is just like the usual <code>TestAssert<\/code>\u00a0command, except that the test is expected to fail and a reason must be provided &#8211; usually the associated bug number. So if you&#8217;re feeling adventurous,\u00a0submit a broken test next time you report a bug.<\/p>\n<p>So there you have it, an extremely subjective run down of the various types of tests we run on every single pull request to ensure it won&#8217;t cause problems when merged. We now ask for tests to be added to code changes whenever it is possible. There is no reason why tests can&#8217;t be added on their own however, so if you spot a gap in our coverage please consider submitting a test for it! This is potentially one of the simplest and best ways to contribute to LiveCode, and also getting you <a href=\"https:\/\/hacktoberfest.digitalocean.com\/\">Hacktoberfest<\/a> t-shirt if you contribute this month.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I really enjoy writing tests. It&#8217;s great to ensure that a new piece of functionality will never be broken, or that a bug will never be reintroduced. It&#8217;s doubly satisfying when something previously untestable becomes testable, when a certain level of struggle or ingenuity is required to work out how\u00a0to test something, or when a<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"om_disable_all_campaigns":false,"footnotes":""},"categories":[45],"tags":[84,69],"class_list":["post-20795","post","type-post","status-publish","format-standard","hentry","category-blog","tag-quality","tag-testing"],"acf":[],"_links":{"self":[{"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/posts\/20795","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/comments?post=20795"}],"version-history":[{"count":12,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/posts\/20795\/revisions"}],"predecessor-version":[{"id":23845,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/posts\/20795\/revisions\/23845"}],"wp:attachment":[{"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/media?parent=20795"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/categories?post=20795"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/legacy.livecode.com\/wp-json\/wp\/v2\/tags?post=20795"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}