diff --git a/404.html b/404.html index 3dd63a9dd..db3a8a1b3 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ Page Not Found | Geo Garden Club - + diff --git a/assets/js/1e5c498d.558820c4.js b/assets/js/1e5c498d.558820c4.js deleted file mode 100644 index c5b2bc90c..000000000 --- a/assets/js/1e5c498d.558820c4.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:()=>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 experience 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.jsx)(t.p,{children:"Before you can run the test suite, bring up the iOS simulator and verify that the GGC source code can run on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.) Once you've verified that GGC loads on the iOS simulator, you can stop execution of GGC if you want. (If you forget to do this, don't worry, invoking the test suite should stop execution of any running app on the simulator automatically.) Don't quit the simulator, however."}),"\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 successfully using 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 allow pasting in order 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 obtain 100% coverage of the app code, in fact that would be impossible, because the code that accesses external services (database, authentication, photos) will never be executed due to the mocking process. For example, the coverage report shows that code in the "data/" subdirectories typically has low coverage.'}),"\n",(0,i.jsx)(t.p,{children:'Instead, use coverage to easily discover "holes" in our test suite, i.e. significant UI code that the test cases do not currently exercise.'}),"\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 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:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\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.ea2efb6d.js b/assets/js/1e5c498d.ea2efb6d.js new file mode 100644 index 000000000..6da164d2c --- /dev/null +++ b/assets/js/1e5c498d.ea2efb6d.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:()=>c,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var i=n(5893),a=n(1151);const s={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,a.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 experience 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.jsx)(t.p,{children:"Before you can run the test suite, bring up the iOS simulator and verify that the GGC source code can run on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.) Once you've verified that GGC loads on the iOS simulator, you can stop execution of GGC if you want. (If you forget to do this, don't worry, invoking the test suite should stop execution of any running app on the simulator automatically.) Don't quit the simulator, however."}),"\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.......: 61.8% (7922 of 12828 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, the 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 successfully using 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, not to verify that the tests achieve 100% coverage (more on this below)."}),"\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 allow pasting in order 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 be useful to see the coverage of our test cases. 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:'The above report was generated when the test cases yielded around 60% coverage. From examining the coverage report, there are some low-hanging fruit that would improve the quality of the test suite significantly: the garden details "filter", timeline management, the help pages, bed management, and so forth.'}),"\n",(0,i.jsx)(t.p,{children:'We are not trying to obtain 100% coverage of the app code, in fact that would be impossible, because the code that accesses external services (database, authentication, photos) will never be executed due to the mocking process. For example, the coverage report shows that code in the "data/" subdirectories typically has low to zero coverage.'}),"\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 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:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\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,a.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 a={},s=i.createContext(a);function r(e){const t=i.useContext(s);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(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.1fe465a1.js b/assets/js/runtime~main.5942a9f5.js similarity index 99% rename from assets/js/runtime~main.1fe465a1.js rename to assets/js/runtime~main.5942a9f5.js index 97dac6882..4258fb2bf 100644 --- a/assets/js/runtime~main.1fe465a1.js +++ b/assets/js/runtime~main.5942a9f5.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:"558820c4",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:"8e9fe34f",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:"ea2efb6d",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:"8e9fe34f",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 Blog | Geo Garden Club - + diff --git a/blog/2023/02/10/welcome.html b/blog/2023/02/10/welcome.html index e3564ce29..6e78e32ee 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 db701cec5..fdc54c3a5 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 f84235f22..a52a2b797 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 dda377b70..5c075c946 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 7b178ba75..c27893ed0 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 26ae858ce..17d8d8f12 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 9c03f2144..ea78754b7 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 c9a2bccbc..3a3f2f002 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 ec9cb9144..77c55746c 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 1080f89c0..59cc13692 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 b3d62541b..1122af5d0 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 f8b04054e..a84c87c09 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 2550d7acf..e0d9f648d 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 b35284d7a..ceed5f07d 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 675319fdc..9a678afc8 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 86a8bf214..b05ea1002 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 6d07f6972..91dc2d146 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 fd510831c..955bbc295 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 f38080c27..ff0a7034b 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 c970103cc..5b7e0139a 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 4cfccf0b9..a22ac329d 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 62a82cbd3..13187d4a5 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 099bf601f..570ec0c82 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 c935ef6d0..3a6484fe5 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 24e73e6ac..8943a14b5 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 9ffb8d30e..64b2fd5e9 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 1fbe2b476..d92d1923f 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 e17973419..5c35a2f43 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 7aaa41ca5..acfd7d7ca 100644 --- a/docs/develop/testing.html +++ b/docs/develop/testing.html @@ -5,7 +5,7 @@ Testing | Geo Garden Club - + @@ -27,8 +27,8 @@

Run the tests

Before you can run the test suite, bring up the iOS simulator and verify that the GGC source code can run on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.) Once you've verified that GGC loads on the iOS simulator, you can stop execution of GGC if you want. (If you forget to do this, don't worry, invoking the test suite should stop execution of any running app on the simulator automatically.) Don't quit the simulator, however.

To run the test suite, invoke ./run_tests.sh. It should take around 4 minutes to run, and should produce output similar to the following:

-
~/GitHub/geogardenclub/ggc_app git:[issue-235]
./run_tests.sh
+ flutter test integration_test/app_test.dart --coverage
00: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
00:48 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.6s
Xcode build done. 33.2s
00:54 +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
Testing settings feature
Testing task feature
Testing variety feature
02:00 +1: All tests passed!
+ genhtml -q coverage/lcov.info -o coverage/html
Overall coverage rate:
source files: 472
lines.......: 35.0% (4464 of 12740 lines)
functions...: no data found
Message summary:
no messages were reported
-

If the tests do not run successfully, output will look similar to this:

+
~/GitHub/geogardenclub/ggc_app git:[issue-235]
./run_tests.sh
+ flutter test integration_test/app_test.dart --coverage
00: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
00:48 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.6s
Xcode build done. 33.2s
00:54 +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
Testing settings feature
Testing task feature
Testing variety feature
02:00 +1: All tests passed!
+ genhtml -q coverage/lcov.info -o coverage/html
Overall coverage rate:
source files: 472
lines.......: 61.8% (7922 of 12828 lines)
functions...: no data found
Message summary:
no messages were reported
+

If the tests do not run successfully, the 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 successfully using the iPhone 15 simulator under iOS 17.5.

@@ -37,7 +37,7 @@

Run the tests<
  • 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.
  • Our test architecture is organized around features.
  • -
  • We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested.
  • +
  • We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below).
  • Always monitor the iOS simulator!

    warning

    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:

    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 allow pasting in order to let the tests proceed. Otherwise, the test process will hang indefinitely.

    This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time.

    @@ -86,11 +86,11 @@

    Coverage

    -

    It can sometimes be interesting to look at the coverage of testing. After running the test suite, you can open the file coverage/html/index.html, which will look similar to this:

    +

    It can be useful to see the coverage of our test cases. After running the test suite, you can open the file coverage/html/index.html, which will look similar to this:

    There are clickable links that you can use to drill down to see which statements have been executed and which have not been.

    -

    Use coverage information wisely. We are not trying to obtain 100% coverage of the app code, in fact that would be impossible, because the code that accesses external services (database, authentication, photos) will never be executed due to the mocking process. For example, the coverage report shows that code in the "data/" subdirectories typically has low coverage.

    -

    Instead, use coverage to easily discover "holes" in our test suite, i.e. significant UI code that the test cases do not currently exercise.

    +

    The above report was generated when the test cases yielded around 60% coverage. From examining the coverage report, there are some low-hanging fruit that would improve the quality of the test suite significantly: the garden details "filter", timeline management, the help pages, bed management, and so forth.

    +

    We are not trying to obtain 100% coverage of the app code, in fact that would be impossible, because the code that accesses external services (database, authentication, photos) will never be executed due to the mocking process. For example, the coverage report shows that code in the "data/" subdirectories typically has low to zero coverage.

    Test Design Hints

    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.

    diff --git a/docs/home/food-security.html b/docs/home/food-security.html index 6d1e1259e..81e5b5d4b 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 a9824241b..00a764187 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 da097759a..828dbc0e8 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 c60b0e588..ee1939fa6 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 291655f69..17cf3eb00 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 30ad4c4f1..f36a67dc0 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 dfe30dddc..2be0f7a08 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 6575f9c76..6a29a5c43 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 3c8c09fc1..a2d5d2a40 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 e0f5a42ff..b8ce5cd10 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 1c8e53e1d..b959c314c 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 321ed644f..c9c2a8c45 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 97379cd07..d2e4ffa81 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 cb9f156cb..a09f8f32c 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 05ddf49a8..b8db75e28 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 f5d235d02..b2fe3f1bd 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 57288a481..1bec74b8a 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 8b344a33a..76ae9b337 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 3761b421b..1fb3549b5 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 5ef753227..d3dbc0d2b 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 d078357f1..e6b63fc14 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 b59a23bf5..86ebc3968 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 208910299..95d16c0b7 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 86d2efd3f..257ebf018 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 93c455da1..251020a4b 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 e26a36d84..6ec0e7766 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/img/develop/testing/coverage.png b/img/develop/testing/coverage.png index bda9b54fc..80a378dc5 100644 Binary files a/img/develop/testing/coverage.png and b/img/develop/testing/coverage.png differ diff --git a/index.html b/index.html index f4d945239..5413b5e89 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 612f1c7fc..bd4403ff3 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,7 +5,7 @@ Markdown page example | Geo Garden Club - +