Page Not Found | Geo Garden Club
-
+
diff --git a/assets/js/1e5c498d.2f0dba15.js b/assets/js/1e5c498d.2f0dba15.js
new file mode 100644
index 00000000..c2bd947e
--- /dev/null
+++ b/assets/js/1e5c498d.2f0dba15.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1217],{798:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>d});var i=n(5893),s=n(1151);const a={hide_table_of_contents:!1},r="Testing",o={id:"develop/testing",title:"Testing",description:"The current goal of testing in GeoGardenClub is to minimize the risk of catastrophic regression from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:",source:"@site/docs/develop/testing.md",sourceDirName:"develop",slug:"/develop/testing",permalink:"/docs/develop/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Backups",permalink:"/docs/develop/backups"}},l={},d=[{value:"Run the tests",id:"run-the-tests",level:2},{value:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",level:2},{value:"About app_test.dart",id:"about-app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"About run_tests_single.sh and app_test_single.dart",id:"about-run_tests_singlesh-and-app_test_singledart",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing excludes many important issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"External service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experiece using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by testing. A decent test suite should enable us to incrementally improve the quality of the code over time."}),"\n",(0,i.jsx)(t.h2,{id:"run-the-tests",children:"Run the tests"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". It should take around 4 minutes to run, and should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app git:[issue-235]\n./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:15 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:41 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:48 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.6s\nXcode build done. 33.2s\n00:54 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\nTesting settings feature\nTesting task feature\nTesting variety feature\n02:00 +1: All tests passed! \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 35.0% (4464 of 12740 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:"If the tests do not run successfully, output will look similar to this:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:33 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:40 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s\nXcode build done. 35.9s\n00:46 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\n\u2550\u2550\u2561 EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following TestFailure was thrown running a test:\nExpected: \n Actual: \n\nWhen the exception was thrown, this was the stack:\n#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)\n\n#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)\n\n#6 main.. (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)\n\n#7 patrolWidgetTest. (package:patrol_finders/src/common.dart:50:7)\n\n#8 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15)\n\n#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24\nThe test description was:\n Fixture 1 Tests\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] \n Test failed. See exception logs above.\n The test description was: Fixture 1 Tests\n \n\nTo run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'\n03:16 +0 -1: Some tests failed. \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 54.8% (7029 of 12823 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:"You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart."}),"\n",(0,i.jsx)(t.p,{children:"If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfullyusing the iPhone 15 simulator under iOS 17.5."}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways from this test execution output:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"If testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to let the tests proceed. Otherwise, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"about-app_testdart",children:"About app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"about-run_tests_singlesh-and-app_test_singledart",children:"About run_tests_single.sh and app_test_single.dart"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can sometimes be interesting to look at the coverage of testing. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),", which will look similar to this:"]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'Use coverage information wisely. We are not trying to get to 100% coverage, because we do not have the resources to build or maintain the test code that would be required for this. That said, we don\'t want large "holes" in our test suite, such that significant aspects of the UI are never exercised.'}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Document the test's navigation path with ",(0,i.jsx)(t.code,{children:"expect ($().visible, equals(true))"}),"."]})," It is possible to write a test with mostly ",(0,i.jsx)(t.code,{children:"await"})," statements such as the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\nawait $('Details').tap();\nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["That code is hard to follow (and potentially harder to debug and maintain) because it does not ever indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts ",(0,i.jsx)(t.code,{children:"expect"})," statements each time the test reaches a new page. This makes it easier to understand the test process:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\n\n// Now at HomeScreenGardensView\nexpect($(HomeScreenGardensView).visible, equals(true)); \nawait $('Details').tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\n\n// Now at CopyPlanting Screen\nexpect($(CopyPlantingScreen).visible, equals(true)); \nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \n"})}),"\n",(0,i.jsx)(t.p,{children:"You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires MacOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const s={},a=i.createContext(s);function r(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/1e5c498d.6157eb20.js b/assets/js/1e5c498d.6157eb20.js
deleted file mode 100644
index c4298bba..00000000
--- a/assets/js/1e5c498d.6157eb20.js
+++ /dev/null
@@ -1 +0,0 @@
-"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1217],{798:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>c,frontMatter:()=>a,metadata:()=>o,toc:()=>d});var i=n(5893),s=n(1151);const a={hide_table_of_contents:!1},r="Testing",o={id:"develop/testing",title:"Testing",description:"The current goal of testing in GeoGardenClub is to minimize the risk of catastrophic regression from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:",source:"@site/docs/develop/testing.md",sourceDirName:"develop",slug:"/develop/testing",permalink:"/docs/develop/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Backups",permalink:"/docs/develop/backups"}},l={},d=[{value:"Run the tests",id:"run-the-tests",level:2},{value:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",level:2},{value:"About app_test.dart",id:"about-app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"About run_tests_single.sh and app_test_single.dart",id:"about-run_tests_singlesh-and-app_test_singledart",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing excludes many important issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"External service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experiece using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by testing. A decent test suite should enable us to incrementally improve the quality of the code over time."}),"\n",(0,i.jsx)(t.h2,{id:"run-the-tests",children:"Run the tests"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". It should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app git:[issue-235]\n./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:15 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:41 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:48 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.6s\nXcode build done. 33.2s\n00:54 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\nTesting settings feature\nTesting task feature\nTesting variety feature\n02:00 +1: All tests passed! \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 35.0% (4464 of 12740 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:"If the tests do not run successfully, it might be because the tests don't work on all devices. At the time of writing, the tests ran successfully using the iPhone 15 simulator under iOS 17.5."}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"If testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to let the tests proceed. Otherwise, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"about-app_testdart",children:"About app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"about-run_tests_singlesh-and-app_test_singledart",children:"About run_tests_single.sh and app_test_single.dart"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can sometimes be interesting to look at the coverage of testing. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),", which will look similar to this:"]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'Use coverage information wisely. We are not trying to get to 100% coverage, because we do not have the resources to build or maintain the test code that would be required for this. That said, we don\'t want large "holes" in our test suite, such that significant aspects of the UI are never exercised.'}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires MacOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function c(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const s={},a=i.createContext(s);function r(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/runtime~main.49274be9.js b/assets/js/runtime~main.6be24b3f.js
similarity index 99%
rename from assets/js/runtime~main.49274be9.js
rename to assets/js/runtime~main.6be24b3f.js
index 054107bd..78d8af18 100644
--- a/assets/js/runtime~main.49274be9.js
+++ b/assets/js/runtime~main.6be24b3f.js
@@ -1 +1 @@
-(()=>{"use strict";var e,a,c,f,d,b={},r={};function t(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return b[e].call(c.exports,c,c.exports,t),c.exports}t.m=b,e=[],t.O=(a,c,f,d)=>{if(!c){var b=1/0;for(i=0;i=d)&&Object.keys(t.O).every((e=>t.O[e](c[o])))?c.splice(o--,1):(r=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},t.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,t.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);t.r(d);var b={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,t.d(d,b),d},t.d=(e,a)=>{for(var c in a)t.o(a,c)&&!t.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},t.f={},t.e=e=>Promise.all(Object.keys(t.f).reduce(((a,c)=>(t.f[c](e,a),a)),[])),t.u=e=>"assets/js/"+({1:"ff0b8175",183:"96083bb9",186:"a8521eb9",196:"505d7517",234:"39838d4e",403:"3ad77611",814:"95f4d37c",825:"7be5f79d",860:"2cd8ab24",1217:"1e5c498d",1340:"e2299c6d",1420:"6741c1a9",1549:"1506d638",1585:"2450005c",1666:"c401bc0d",1744:"10921c5b",1937:"49882d99",2446:"bc1f8660",2522:"c7c467a1",2535:"814f3328",2743:"968b4846",2889:"a1e7621f",2967:"e4eb6786",3085:"1f391b9e",3089:"a6aa9e1f",3560:"17d8eee1",3608:"9e4087bc",3629:"aba21aa0",3844:"772c3429",4e3:"afc29949",4031:"f81c1134",4057:"c03baef0",4063:"31caa863",4076:"7d1225b6",4088:"0058b4c6",4195:"c4f5d8e4",4368:"a94703ab",4524:"e27695c2",4713:"bc03b1b7",5014:"38346c4b",5857:"3e240fbf",5980:"a7456010",6103:"ccc49370",6142:"bebdd554",6265:"906ac375",6414:"3d832522",6427:"59628a4d",6642:"c15d9823",6800:"2f9db241",6906:"9ebba4ea",6957:"a5b8d3e9",6974:"af21c641",7222:"0bd3a280",7346:"f3759001",7393:"acecf23e",7414:"393be207",7540:"0f1af657",7664:"reactPlayerPreview",7918:"17896441",7937:"c48bbb24",8294:"3463d78f",8392:"ed0568ab",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",9208:"36994c47",9256:"3bea7cd1",9268:"ba771284",9572:"7d56ced7",9586:"3b4579e8",9601:"18fc9463",9661:"5e95c892",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{1:"a06d7a3a",183:"d2117bbc",186:"4c804139",196:"e701aaa5",234:"76946f78",403:"dafd6774",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1217:"6157eb20",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"ffe69db6",1744:"4d956c8c",1772:"3d06e0e2",1937:"96ed6c84",2446:"74e6e406",2522:"79a7edaa",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2889:"4e529702",2967:"a7c4ab03",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"84e52c31",4e3:"1038aca8",4031:"6f8546d9",4057:"efd80a40",4063:"ead42224",4076:"82c0a513",4088:"0a92b2c1",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"802bf1c8",5014:"bd156c61",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"2704a200",6642:"b756708b",6800:"2dc6905e",6906:"df274206",6957:"07f3c936",6974:"242c3e8d",7222:"7b0ad8f2",7346:"ecb01d32",7393:"9d35c647",7414:"37f8208f",7540:"5d4247b4",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"97dc70b8",8392:"a5d4194d",8518:"eaa77d27",8653:"1c9ade88",8754:"f012b88f",9208:"203bad01",9256:"0698ba6b",9268:"f1b885cf",9572:"3bfef52c",9586:"82224d7d",9601:"e6c986e7",9661:"2dcb0623",9866:"6c00f0dc",9929:"e36f6a83"}[e]+".js",t.miniCssF=e=>{},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="geogardenclub-github-io:",t.l=(e,a,c,b)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.p="/",t.gca=function(e){return e={17896441:"7918",ff0b8175:"1","96083bb9":"183",a8521eb9:"186","505d7517":"196","39838d4e":"234","3ad77611":"403","95f4d37c":"814","7be5f79d":"825","2cd8ab24":"860","1e5c498d":"1217",e2299c6d:"1340","6741c1a9":"1420","1506d638":"1549","2450005c":"1585",c401bc0d:"1666","10921c5b":"1744","49882d99":"1937",bc1f8660:"2446",c7c467a1:"2522","814f3328":"2535","968b4846":"2743",a1e7621f:"2889",e4eb6786:"2967","1f391b9e":"3085",a6aa9e1f:"3089","17d8eee1":"3560","9e4087bc":"3608",aba21aa0:"3629","772c3429":"3844",afc29949:"4000",f81c1134:"4031",c03baef0:"4057","31caa863":"4063","7d1225b6":"4076","0058b4c6":"4088",c4f5d8e4:"4195",a94703ab:"4368",e27695c2:"4524",bc03b1b7:"4713","38346c4b":"5014","3e240fbf":"5857",a7456010:"5980",ccc49370:"6103",bebdd554:"6142","906ac375":"6265","3d832522":"6414","59628a4d":"6427",c15d9823:"6642","2f9db241":"6800","9ebba4ea":"6906",a5b8d3e9:"6957",af21c641:"6974","0bd3a280":"7222",f3759001:"7346",acecf23e:"7393","393be207":"7414","0f1af657":"7540",reactPlayerPreview:"7664",c48bbb24:"7937","3463d78f":"8294",ed0568ab:"8392",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","36994c47":"9208","3bea7cd1":"9256",ba771284:"9268","7d56ced7":"9572","3b4579e8":"9586","18fc9463":"9601","5e95c892":"9661","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,t.p+t.u(e)},(()=>{var e={1303:0,532:0};t.f.j=(a,c)=>{var f=t.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var b=t.p+t.u(a),r=new Error;t.l(b,(c=>{if(t.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),b=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",r.name="ChunkLoadError",r.type=d,r.request=b,f[1](r)}}),"chunk-"+a,a)}},t.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,b=c[0],r=c[1],o=c[2],n=0;if(b.some((a=>0!==e[a]))){for(f in r)t.o(r,f)&&(t.m[f]=r[f]);if(o)var i=o(t)}for(a&&a(c);n{"use strict";var e,a,c,f,d,b={},r={};function t(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return b[e].call(c.exports,c,c.exports,t),c.exports}t.m=b,e=[],t.O=(a,c,f,d)=>{if(!c){var b=1/0;for(i=0;i=d)&&Object.keys(t.O).every((e=>t.O[e](c[o])))?c.splice(o--,1):(r=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},t.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return t.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,t.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);t.r(d);var b={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,t.d(d,b),d},t.d=(e,a)=>{for(var c in a)t.o(a,c)&&!t.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},t.f={},t.e=e=>Promise.all(Object.keys(t.f).reduce(((a,c)=>(t.f[c](e,a),a)),[])),t.u=e=>"assets/js/"+({1:"ff0b8175",183:"96083bb9",186:"a8521eb9",196:"505d7517",234:"39838d4e",403:"3ad77611",814:"95f4d37c",825:"7be5f79d",860:"2cd8ab24",1217:"1e5c498d",1340:"e2299c6d",1420:"6741c1a9",1549:"1506d638",1585:"2450005c",1666:"c401bc0d",1744:"10921c5b",1937:"49882d99",2446:"bc1f8660",2522:"c7c467a1",2535:"814f3328",2743:"968b4846",2889:"a1e7621f",2967:"e4eb6786",3085:"1f391b9e",3089:"a6aa9e1f",3560:"17d8eee1",3608:"9e4087bc",3629:"aba21aa0",3844:"772c3429",4e3:"afc29949",4031:"f81c1134",4057:"c03baef0",4063:"31caa863",4076:"7d1225b6",4088:"0058b4c6",4195:"c4f5d8e4",4368:"a94703ab",4524:"e27695c2",4713:"bc03b1b7",5014:"38346c4b",5857:"3e240fbf",5980:"a7456010",6103:"ccc49370",6142:"bebdd554",6265:"906ac375",6414:"3d832522",6427:"59628a4d",6642:"c15d9823",6800:"2f9db241",6906:"9ebba4ea",6957:"a5b8d3e9",6974:"af21c641",7222:"0bd3a280",7346:"f3759001",7393:"acecf23e",7414:"393be207",7540:"0f1af657",7664:"reactPlayerPreview",7918:"17896441",7937:"c48bbb24",8294:"3463d78f",8392:"ed0568ab",8518:"a7bd4aaa",8653:"ec0f34d7",8754:"6b1fc3de",9208:"36994c47",9256:"3bea7cd1",9268:"ba771284",9572:"7d56ced7",9586:"3b4579e8",9601:"18fc9463",9661:"5e95c892",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{1:"a06d7a3a",183:"d2117bbc",186:"4c804139",196:"e701aaa5",234:"76946f78",403:"dafd6774",814:"4e08262b",825:"d42ee47b",860:"d7028e73",1217:"2f0dba15",1340:"11ae0623",1420:"aeb48ccb",1549:"7d1c392d",1585:"db463b44",1666:"ffe69db6",1744:"4d956c8c",1772:"3d06e0e2",1937:"96ed6c84",2446:"74e6e406",2522:"79a7edaa",2535:"0ba125c5",2700:"83aa9a75",2743:"67c47945",2889:"4e529702",2967:"a7c4ab03",3085:"3dba5538",3089:"911b8dd5",3560:"cffc9f0b",3608:"ee0c677a",3629:"eb980bea",3844:"84e52c31",4e3:"1038aca8",4031:"6f8546d9",4057:"efd80a40",4063:"ead42224",4076:"82c0a513",4088:"0a92b2c1",4195:"b92bf9f8",4368:"4aef8496",4524:"f13327a6",4713:"802bf1c8",5014:"bd156c61",5655:"ab3e12ff",5857:"241b5abc",5980:"f93cbc61",6103:"4990621e",6142:"9adb781d",6265:"ecc041e6",6414:"19d9c76b",6427:"2704a200",6642:"b756708b",6800:"2dc6905e",6906:"df274206",6957:"07f3c936",6974:"242c3e8d",7222:"7b0ad8f2",7346:"ecb01d32",7393:"9d35c647",7414:"37f8208f",7540:"5d4247b4",7664:"e5a1011e",7918:"b96e81ff",7937:"881a7b56",8041:"e22d43c6",8294:"97dc70b8",8392:"a5d4194d",8518:"eaa77d27",8653:"1c9ade88",8754:"f012b88f",9208:"203bad01",9256:"0698ba6b",9268:"f1b885cf",9572:"3bfef52c",9586:"82224d7d",9601:"e6c986e7",9661:"2dcb0623",9866:"6c00f0dc",9929:"e36f6a83"}[e]+".js",t.miniCssF=e=>{},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),t.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="geogardenclub-github-io:",t.l=(e,a,c,b)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.p="/",t.gca=function(e){return e={17896441:"7918",ff0b8175:"1","96083bb9":"183",a8521eb9:"186","505d7517":"196","39838d4e":"234","3ad77611":"403","95f4d37c":"814","7be5f79d":"825","2cd8ab24":"860","1e5c498d":"1217",e2299c6d:"1340","6741c1a9":"1420","1506d638":"1549","2450005c":"1585",c401bc0d:"1666","10921c5b":"1744","49882d99":"1937",bc1f8660:"2446",c7c467a1:"2522","814f3328":"2535","968b4846":"2743",a1e7621f:"2889",e4eb6786:"2967","1f391b9e":"3085",a6aa9e1f:"3089","17d8eee1":"3560","9e4087bc":"3608",aba21aa0:"3629","772c3429":"3844",afc29949:"4000",f81c1134:"4031",c03baef0:"4057","31caa863":"4063","7d1225b6":"4076","0058b4c6":"4088",c4f5d8e4:"4195",a94703ab:"4368",e27695c2:"4524",bc03b1b7:"4713","38346c4b":"5014","3e240fbf":"5857",a7456010:"5980",ccc49370:"6103",bebdd554:"6142","906ac375":"6265","3d832522":"6414","59628a4d":"6427",c15d9823:"6642","2f9db241":"6800","9ebba4ea":"6906",a5b8d3e9:"6957",af21c641:"6974","0bd3a280":"7222",f3759001:"7346",acecf23e:"7393","393be207":"7414","0f1af657":"7540",reactPlayerPreview:"7664",c48bbb24:"7937","3463d78f":"8294",ed0568ab:"8392",a7bd4aaa:"8518",ec0f34d7:"8653","6b1fc3de":"8754","36994c47":"9208","3bea7cd1":"9256",ba771284:"9268","7d56ced7":"9572","3b4579e8":"9586","18fc9463":"9601","5e95c892":"9661","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,t.p+t.u(e)},(()=>{var e={1303:0,532:0};t.f.j=(a,c)=>{var f=t.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var b=t.p+t.u(a),r=new Error;t.l(b,(c=>{if(t.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),b=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",r.name="ChunkLoadError",r.type=d,r.request=b,f[1](r)}}),"chunk-"+a,a)}},t.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,b=c[0],r=c[1],o=c[2],n=0;if(b.some((a=>0!==e[a]))){for(f in r)t.o(r,f)&&(t.m[f]=r[f]);if(o)var i=o(t)}for(a&&a(c);nBlog | Geo Garden Club
-
+
diff --git a/blog/2023/02/10/welcome.html b/blog/2023/02/10/welcome.html
index 653ac1cd..9bc2069b 100644
--- a/blog/2023/02/10/welcome.html
+++ b/blog/2023/02/10/welcome.html
@@ -5,7 +5,7 @@
Welcome, Geo Garden Club! Aloha, Agile Garden Club! | Geo Garden Club
-
+
diff --git a/blog/archive.html b/blog/archive.html
index ff5c8617..e0dae65e 100644
--- a/blog/archive.html
+++ b/blog/archive.html
@@ -5,7 +5,7 @@
Archive | Geo Garden Club
-
+
diff --git a/docs/business.html b/docs/business.html
index 65302419..004d2ff8 100644
--- a/docs/business.html
+++ b/docs/business.html
@@ -5,7 +5,7 @@
Welcome to the GGC Business Development Guide | Geo Garden Club
-
+
diff --git a/docs/business/market-size.html b/docs/business/market-size.html
index e25d6303..9fb9dab7 100644
--- a/docs/business/market-size.html
+++ b/docs/business/market-size.html
@@ -5,7 +5,7 @@
Market Size Estimation (USA) | Geo Garden Club
-
+
diff --git a/docs/business/milestones.html b/docs/business/milestones.html
index 7bf349e5..3beef51d 100644
--- a/docs/business/milestones.html
+++ b/docs/business/milestones.html
@@ -5,7 +5,7 @@
Milestones | Geo Garden Club
-
+
diff --git a/docs/business/roadmap.html b/docs/business/roadmap.html
index 5be116c7..3dd54c83 100644
--- a/docs/business/roadmap.html
+++ b/docs/business/roadmap.html
@@ -5,7 +5,7 @@
Roadmap | Geo Garden Club
-
+
diff --git a/docs/develop.html b/docs/develop.html
index 8a84d57d..180ce5d1 100644
--- a/docs/develop.html
+++ b/docs/develop.html
@@ -5,7 +5,7 @@
Welcome to the GGC Developers Guide | Geo Garden Club
-
+
diff --git a/docs/develop/architecture.html b/docs/develop/architecture.html
index a8640592..529213f3 100644
--- a/docs/develop/architecture.html
+++ b/docs/develop/architecture.html
@@ -5,7 +5,7 @@
Architecture | Geo Garden Club
-
+
diff --git a/docs/develop/backups.html b/docs/develop/backups.html
index c21da9a3..557dd3a1 100644
--- a/docs/develop/backups.html
+++ b/docs/develop/backups.html
@@ -5,7 +5,7 @@
Backups | Geo Garden Club
-
+
diff --git a/docs/develop/coding-standards.html b/docs/develop/coding-standards.html
index 75eb0de1..c8ae6264 100644
--- a/docs/develop/coding-standards.html
+++ b/docs/develop/coding-standards.html
@@ -5,7 +5,7 @@
Coding Standards | Geo Garden Club
-
+
diff --git a/docs/develop/deployment.html b/docs/develop/deployment.html
index 40dc9377..b87f2f79 100644
--- a/docs/develop/deployment.html
+++ b/docs/develop/deployment.html
@@ -5,7 +5,7 @@
Deployment | Geo Garden Club
-
+
diff --git a/docs/develop/design/badges.html b/docs/develop/design/badges.html
index 6b67f29f..d7f3b568 100644
--- a/docs/develop/design/badges.html
+++ b/docs/develop/design/badges.html
@@ -5,7 +5,7 @@
Badges | Geo Garden Club
-
+
diff --git a/docs/develop/design/data-model-old.html b/docs/develop/design/data-model-old.html
index 23645df1..9a190d87 100644
--- a/docs/develop/design/data-model-old.html
+++ b/docs/develop/design/data-model-old.html
@@ -5,7 +5,7 @@
Data Model | Geo Garden Club
-
+
diff --git a/docs/develop/design/data-model.html b/docs/develop/design/data-model.html
index 7de05a18..fd5c7590 100644
--- a/docs/develop/design/data-model.html
+++ b/docs/develop/design/data-model.html
@@ -5,7 +5,7 @@
Data Model | Geo Garden Club
-
+
diff --git a/docs/develop/design/data-mutation.html b/docs/develop/design/data-mutation.html
index 312b60e4..34ddb8e1 100644
--- a/docs/develop/design/data-mutation.html
+++ b/docs/develop/design/data-mutation.html
@@ -5,7 +5,7 @@
Data Mutation | Geo Garden Club
-
+
diff --git a/docs/develop/design/input-fields.html b/docs/develop/design/input-fields.html
index 4ca54d1c..70a4bd14 100644
--- a/docs/develop/design/input-fields.html
+++ b/docs/develop/design/input-fields.html
@@ -5,7 +5,7 @@
GGC Input Fields | Geo Garden Club
-
+
diff --git a/docs/develop/design/with-widgets.html b/docs/develop/design/with-widgets.html
index 6753d55c..3954c157 100644
--- a/docs/develop/design/with-widgets.html
+++ b/docs/develop/design/with-widgets.html
@@ -5,7 +5,7 @@
"With" widgets | Geo Garden Club
-
+
diff --git a/docs/develop/installation.html b/docs/develop/installation.html
index 3bc1a45b..1eb2f4d4 100644
--- a/docs/develop/installation.html
+++ b/docs/develop/installation.html
@@ -5,7 +5,7 @@
Installation | Geo Garden Club
-
+
diff --git a/docs/develop/onboarding.html b/docs/develop/onboarding.html
index b13ca2ef..156151e7 100644
--- a/docs/develop/onboarding.html
+++ b/docs/develop/onboarding.html
@@ -5,7 +5,7 @@
Onboarding | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-0.0/chatgpt-feedback.html b/docs/develop/releases/release-0.0/chatgpt-feedback.html
index 6dc77d9d..bc3f8850 100644
--- a/docs/develop/releases/release-0.0/chatgpt-feedback.html
+++ b/docs/develop/releases/release-0.0/chatgpt-feedback.html
@@ -5,7 +5,7 @@
ChatGPT feedback | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-0.0/customer-feedback.html b/docs/develop/releases/release-0.0/customer-feedback.html
index 045574ef..19875ea3 100644
--- a/docs/develop/releases/release-0.0/customer-feedback.html
+++ b/docs/develop/releases/release-0.0/customer-feedback.html
@@ -5,7 +5,7 @@
Customer feedback | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-0.0/design.html b/docs/develop/releases/release-0.0/design.html
index 21f8263d..1b4d884e 100644
--- a/docs/develop/releases/release-0.0/design.html
+++ b/docs/develop/releases/release-0.0/design.html
@@ -5,7 +5,7 @@
Design and implementation | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-0.0/entrepreneur-feedback.html b/docs/develop/releases/release-0.0/entrepreneur-feedback.html
index 1e956d41..3df7737b 100644
--- a/docs/develop/releases/release-0.0/entrepreneur-feedback.html
+++ b/docs/develop/releases/release-0.0/entrepreneur-feedback.html
@@ -5,7 +5,7 @@
Entrepreneur feedback | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-1.0/cvp.html b/docs/develop/releases/release-1.0/cvp.html
index 2247ff2f..90410468 100644
--- a/docs/develop/releases/release-1.0/cvp.html
+++ b/docs/develop/releases/release-1.0/cvp.html
@@ -5,7 +5,7 @@
Core Value Propositions | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-1.0/end-of-season-feedback.html b/docs/develop/releases/release-1.0/end-of-season-feedback.html
index 1729e559..5fa9eae5 100644
--- a/docs/develop/releases/release-1.0/end-of-season-feedback.html
+++ b/docs/develop/releases/release-1.0/end-of-season-feedback.html
@@ -5,7 +5,7 @@
End of Season Feedback | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-1.0/goals.html b/docs/develop/releases/release-1.0/goals.html
index d4975ffe..ff95cd71 100644
--- a/docs/develop/releases/release-1.0/goals.html
+++ b/docs/develop/releases/release-1.0/goals.html
@@ -5,7 +5,7 @@
Technology Goals | Geo Garden Club
-
+
diff --git a/docs/develop/releases/release-1.0/onboarding-feedback.html b/docs/develop/releases/release-1.0/onboarding-feedback.html
index 28a600fc..77240103 100644
--- a/docs/develop/releases/release-1.0/onboarding-feedback.html
+++ b/docs/develop/releases/release-1.0/onboarding-feedback.html
@@ -5,7 +5,7 @@
Onboarding Feedback | Geo Garden Club
-
+
diff --git a/docs/develop/scripts.html b/docs/develop/scripts.html
index 5350e1fd..53b35e56 100644
--- a/docs/develop/scripts.html
+++ b/docs/develop/scripts.html
@@ -5,7 +5,7 @@
Scripts | Geo Garden Club
-
+
diff --git a/docs/develop/testing.html b/docs/develop/testing.html
index caef5655..847d97f4 100644
--- a/docs/develop/testing.html
+++ b/docs/develop/testing.html
@@ -5,7 +5,7 @@
Testing | Geo Garden Club
-
+
@@ -25,10 +25,13 @@
Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by testing. A decent test suite should enable us to incrementally improve the quality of the code over time.
If the tests do not run successfully, it might be because the tests don't work on all devices. At the time of writing, the tests ran successfully using the iPhone 15 simulator under iOS 17.5.
-
Here are some important takeaways:
+
If the tests do not run successfully, output will look similar to this:
+
./run_tests.sh + flutter test integration_test/app_test.dart --coverage 00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:33 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 00:40 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s Xcode build done. 35.9s 00:46 +0: GGC Integration Test (All) Fixture 1 Tests Testing admin feature Testing badge feature Testing chapter feature Testing chat feature Testing crop feature Testing garden feature Testing gardener feature Testing geobot feature Testing home feature Testing observation feature Testing outcome feature Testing planting feature ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following TestFailure was thrown running a test: Expected: <true> Actual: <false> When the exception was thrown, this was the stack: #4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3) <asynchronous suspension> #5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3) <asynchronous suspension> #6 main.<anonymous closure>.<anonymous closure> (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7) <asynchronous suspension> #7 patrolWidgetTest.<anonymous closure> (package:patrol_finders/src/common.dart:50:7) <asynchronous suspension> #8 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15) <asynchronous suspension> #9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5) <asynchronous suspension> <asynchronous suspension> (elided one frame from package:stack_trace) This was caught by the test expectation on the following line: file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24 The test description was: Fixture 1 Tests ════════════════════════════════════════════════════════════════════════════════════════════════════ 03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] Test failed. See exception logs above. The test description was: Fixture 1 Tests To run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests' 03:16 +0 -1: Some tests failed. + genhtml -q coverage/lcov.info -o coverage/html Overall coverage rate: source files: 472 lines.......: 54.8% (7029 of 12823 lines) functions...: no data found Message summary: no messages were reported
+
You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart.
+
If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfullyusing the iPhone 15 simulator under iOS 17.5.
+
Here are some important takeaways from this test execution output:
We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code.
Our tests run with a specific "test fixture" (currently we're using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in assets/test/fixture1. In the future, we might write tests that require a different fixture.
@@ -90,6 +93,11 @@
Test Desig
Set up a "Run Configuration" to simplify testing. In IntelliJ, make a Run configuration that invokes lib/main_test_fixture.dart so that you can push the green arrow to easily bring up the simulator with the test data loaded into it.
Use the Testing Run Configuration to guide the writing of test code steps. To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it's a one-to-one relationship.
Make sure that you include at least one "expect" statement to verify the results of a behavior. So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow.
+
Document the test's navigation path with expect ($(<screen>).visible, equals(true)). It is possible to write a test with mostly await statements such as the following:
That code is hard to follow (and potentially harder to debug and maintain) because it does not ever indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts expect statements each time the test reaches a new page. This makes it easier to understand the test process:
+
String testPlanting ='Raspberry (Golden)'; awaitgotoDrawerScreen($,HomeScreen); await $(BottomNavigationBar).$('Gardens').tap(); // Now at HomeScreenGardensView expect($(HomeScreenGardensView).visible,equals(true)); await $('Details').tap(); // Now at GardenDetailsScreen expect($(GardenDetailsScreen).visible,equals(true)); expect($(testPlanting).visible,equals(true)); await $(testPlanting).tap(); await $(PlantingDetailsCopyButton).tap(); // Now at CopyPlanting Screen expect($(CopyPlantingScreen).visible,equals(true)); await $(GardenDropdown).tap(); await $('Alderwood').tap(); await $(BedDropdown).tap(); await $('02').tap(); await $('Submit').scrollTo().tap(); // Now at GardenDetailsScreen expect($(GardenDetailsScreen).visible,equals(true));
+
You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen.
Don't use an absolute "count" of items to do verification. For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification.
Don't delete or modify any entities in the test fixture. If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be new entities added, all tests can assume that the entities in the test fixture will be there exactly as defined.
Don't write too much test code. Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can't spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature's UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you've written a very helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something.
diff --git a/docs/home/food-security.html b/docs/home/food-security.html
index ceed8784..0befe9d4 100644
--- a/docs/home/food-security.html
+++ b/docs/home/food-security.html
@@ -5,7 +5,7 @@
Food Security | Geo Garden Club
-
+
diff --git a/docs/home/innovations.html b/docs/home/innovations.html
index a01b24b1..0da353cc 100644
--- a/docs/home/innovations.html
+++ b/docs/home/innovations.html
@@ -5,7 +5,7 @@
Design Innovations | Geo Garden Club
-
+
diff --git a/docs/home/related-work.html b/docs/home/related-work.html
index ecfd1cb7..d8971fc0 100644
--- a/docs/home/related-work.html
+++ b/docs/home/related-work.html
@@ -5,7 +5,7 @@
Garden Planning Tools | Geo Garden Club
-
+
diff --git a/docs/home/serious-gardeners.html b/docs/home/serious-gardeners.html
index 6e51a4ea..c19c0484 100644
--- a/docs/home/serious-gardeners.html
+++ b/docs/home/serious-gardeners.html
@@ -5,7 +5,7 @@
"Serious" Gardeners | Geo Garden Club
-
+
diff --git a/docs/home/sneak-peek.html b/docs/home/sneak-peek.html
index cd10256a..3dbe61e0 100644
--- a/docs/home/sneak-peek.html
+++ b/docs/home/sneak-peek.html
@@ -5,7 +5,7 @@
Mobile App Sneak Peek | Geo Garden Club
-
+
diff --git a/docs/home/team.html b/docs/home/team.html
index 3a4fcd01..035c512f 100644
--- a/docs/home/team.html
+++ b/docs/home/team.html
@@ -5,7 +5,7 @@
The Team | Geo Garden Club
-
+
diff --git a/docs/home/welcome.html b/docs/home/welcome.html
index ce4120e7..0648b260 100644
--- a/docs/home/welcome.html
+++ b/docs/home/welcome.html
@@ -5,7 +5,7 @@
Welcome | Geo Garden Club
-
+
diff --git a/docs/user-guide/adding-plantings.html b/docs/user-guide/adding-plantings.html
index 882190c8..283c9cc4 100644
--- a/docs/user-guide/adding-plantings.html
+++ b/docs/user-guide/adding-plantings.html
@@ -5,7 +5,7 @@
Add Plantings to Beds | Geo Garden Club
-
+
diff --git a/docs/user-guide/adding-vendors-crops-varieties.html b/docs/user-guide/adding-vendors-crops-varieties.html
index 98d44d4c..732c4299 100644
--- a/docs/user-guide/adding-vendors-crops-varieties.html
+++ b/docs/user-guide/adding-vendors-crops-varieties.html
@@ -5,7 +5,7 @@
Add Crops, Varieties, Vendors to the Chapter Database | Geo Garden Club
-
+
diff --git a/docs/user-guide/badges.html b/docs/user-guide/badges.html
index 64297a78..513bf4ca 100644
--- a/docs/user-guide/badges.html
+++ b/docs/user-guide/badges.html
@@ -5,7 +5,7 @@
Badges | Geo Garden Club
-
+
diff --git a/docs/user-guide/chat-rooms.html b/docs/user-guide/chat-rooms.html
index b19dcbc3..64be19ed 100644
--- a/docs/user-guide/chat-rooms.html
+++ b/docs/user-guide/chat-rooms.html
@@ -5,7 +5,7 @@
Chat Rooms | Geo Garden Club
-
+
diff --git a/docs/user-guide/define-a-garden.html b/docs/user-guide/define-a-garden.html
index 37a74116..64ce694e 100644
--- a/docs/user-guide/define-a-garden.html
+++ b/docs/user-guide/define-a-garden.html
@@ -5,7 +5,7 @@
Define a Garden | Geo Garden Club
-
+
diff --git a/docs/user-guide/downloading.html b/docs/user-guide/downloading.html
index edf9bab8..c3fe9c7f 100644
--- a/docs/user-guide/downloading.html
+++ b/docs/user-guide/downloading.html
@@ -5,7 +5,7 @@
Downloading | Geo Garden Club
-
+
diff --git a/docs/user-guide/explore-a-chapter.html b/docs/user-guide/explore-a-chapter.html
index 3825f8e8..0493ee8a 100644
--- a/docs/user-guide/explore-a-chapter.html
+++ b/docs/user-guide/explore-a-chapter.html
@@ -5,7 +5,7 @@
Explore a Chapter | Geo Garden Club
-
+
diff --git a/docs/user-guide/explore-a-garden.html b/docs/user-guide/explore-a-garden.html
index b788ca0a..ccc3c0fd 100644
--- a/docs/user-guide/explore-a-garden.html
+++ b/docs/user-guide/explore-a-garden.html
@@ -5,7 +5,7 @@
Explore a Garden | Geo Garden Club
-
+
diff --git a/docs/user-guide/geobot.html b/docs/user-guide/geobot.html
index 35eec350..73d142eb 100644
--- a/docs/user-guide/geobot.html
+++ b/docs/user-guide/geobot.html
@@ -5,7 +5,7 @@
GeoBot | Geo Garden Club
-
+
diff --git a/docs/user-guide/guided-tour.html b/docs/user-guide/guided-tour.html
index 93b0a08f..e48d1c25 100644
--- a/docs/user-guide/guided-tour.html
+++ b/docs/user-guide/guided-tour.html
@@ -5,7 +5,7 @@
Frequently Asked (Gardening) Questions | Geo Garden Club
-
+
diff --git a/docs/user-guide/observations.html b/docs/user-guide/observations.html
index 6ce5c877..3a7a781e 100644
--- a/docs/user-guide/observations.html
+++ b/docs/user-guide/observations.html
@@ -5,7 +5,7 @@
Observations | Geo Garden Club
-
+
diff --git a/docs/user-guide/outcomes.html b/docs/user-guide/outcomes.html
index ba23caf5..a8c43a91 100644
--- a/docs/user-guide/outcomes.html
+++ b/docs/user-guide/outcomes.html
@@ -5,7 +5,7 @@
outcomes | Geo Garden Club
-
+
diff --git a/docs/user-guide/overview.html b/docs/user-guide/overview.html
index 9c5d8987..0851d698 100644
--- a/docs/user-guide/overview.html
+++ b/docs/user-guide/overview.html
@@ -5,7 +5,7 @@
Overview | Geo Garden Club
-
+
diff --git a/docs/user-guide/privacy.html b/docs/user-guide/privacy.html
index 57a8d35a..7cdb313b 100644
--- a/docs/user-guide/privacy.html
+++ b/docs/user-guide/privacy.html
@@ -5,7 +5,7 @@
Privacy Policy | Geo Garden Club
-
+
diff --git a/docs/user-guide/registration.html b/docs/user-guide/registration.html
index 0ca4adda..1ca7b2ba 100644
--- a/docs/user-guide/registration.html
+++ b/docs/user-guide/registration.html
@@ -5,7 +5,7 @@
Registration | Geo Garden Club
-
+
diff --git a/docs/user-guide/scenarios.html b/docs/user-guide/scenarios.html
index 9a43c7ca..bd339871 100644
--- a/docs/user-guide/scenarios.html
+++ b/docs/user-guide/scenarios.html
@@ -5,7 +5,7 @@
Planting Scenarios | Geo Garden Club
-
+
diff --git a/docs/user-guide/seeds.html b/docs/user-guide/seeds.html
index cc5c965b..978a3f0f 100644
--- a/docs/user-guide/seeds.html
+++ b/docs/user-guide/seeds.html
@@ -5,7 +5,7 @@
Seeds | Geo Garden Club
-
+
diff --git a/docs/user-guide/tasks.html b/docs/user-guide/tasks.html
index 35b6fdf0..4918f117 100644
--- a/docs/user-guide/tasks.html
+++ b/docs/user-guide/tasks.html
@@ -5,7 +5,7 @@
Tasks | Geo Garden Club
-
+
diff --git a/docs/user-guide/terms-and-conditions.html b/docs/user-guide/terms-and-conditions.html
index c2aedae0..07a3def6 100644
--- a/docs/user-guide/terms-and-conditions.html
+++ b/docs/user-guide/terms-and-conditions.html
@@ -5,7 +5,7 @@
Terms and Conditions | Geo Garden Club
-
+
diff --git a/index.html b/index.html
index 3ec4bc49..66e9d703 100644
--- a/index.html
+++ b/index.html
@@ -5,7 +5,7 @@
Geo Garden Club | Geo Garden Club
-
+
diff --git a/markdown-page.html b/markdown-page.html
index 29a483b8..53c47c75 100644
--- a/markdown-page.html
+++ b/markdown-page.html
@@ -5,7 +5,7 @@
Markdown page example | Geo Garden Club
-
+