{"id":17458165,"url":"https://github.com/pocesar/actor-testing","last_synced_at":"2025-03-21T03:33:13.498Z","repository":{"id":53498000,"uuid":"312434822","full_name":"pocesar/actor-testing","owner":"pocesar","description":"Test your actors and tasks with multiple inputs, and expected outputs, integrating with results checker","archived":false,"fork":false,"pushed_at":"2024-06-30T21:21:12.000Z","size":118,"stargazers_count":3,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-18T06:28:58.148Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pocesar.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-13T00:56:27.000Z","updated_at":"2023-03-04T05:53:57.000Z","dependencies_parsed_at":"2024-10-20T19:23:25.293Z","dependency_job_id":null,"html_url":"https://github.com/pocesar/actor-testing","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pocesar%2Factor-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pocesar%2Factor-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pocesar%2Factor-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pocesar%2Factor-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pocesar","download_url":"https://codeload.github.com/pocesar/actor-testing/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221811374,"owners_count":16884305,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-18T03:55:52.987Z","updated_at":"2025-03-21T03:33:13.491Z","avatar_url":"https://github.com/pocesar.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Apify actor testing\n\nTest your actors and tasks with multiple inputs, and expected outputs, integrating with results checker\n\n* [Features](#features)\n* [Testing](#testing)\n* [Expected consumption](#expected-consumption)\n* [Reasoning](#reasoning)\n\n## Features\n\nBy leveraging [Jasmine](https://jasmine.github.io), the extensible `expect` and Apify SDK, you can test tasks and actors,\nand check for their output consistency and/or duplicates.\n\nIt goes well with [monitoring suit](https://apify.com/apify/monitoring) for running your production runs, but this actor should\nbe run in a scheduled manner for best results.\n\n* You can run many tests in parallel or test them in series (as your account memory allows)\n* You can run tests locally but accessing platform storage and actors\n* Abstracts access to two other public actors:\n  * [Results checker](https://apify.com/lukaskrivka/results-checker)\n  * [Duplications checker](https://apify.com/lukaskrivka/duplications-checker)\n\n## Testing\n\nThe testing interface is familiar with Jasmine BDD tests, but with Apify specific async matchers:\n\n```js\n({\n    it,\n    run,\n    expectAsync,\n    input, // Object containing the current input, you can access customData here\n    describe, // describe subsections\n    expect, // default Jasmine expect\n    _, // lodash as a helper to traverse array items and objects\n    moment // Moment.JS to help with dates and time math\n    Apify // Apify SDK v2\n    apifyClient // Apify client v2\n}) =\u003e {\n\n  // describe is not needed, but it's good to keep everything tidy\n  describe('sub', () =\u003e {\n\n    it('should have preconfigured task working', async () =\u003e {\n        const myTaskResult = await run({\n            // actorId: 'actor/from-store', // can use an actorId directly\n            taskId: 'myuser/my-task-name',\n            input: {\n                some: 'extra input' // optional overrides\n            },\n            options: {\n                timeout: 15000 // optional call options\n            },\n            name: 'should have preconfigured task working'\n        });\n\n        // sync assertions, not very useful, expections should have inside async assertions\n        expect(myTaskResult.runId).not.toBeEmptyString();\n\n        /**\n         * Async assertions calls resources on the platform\n         */\n\n        // reads the OUTPUT key\n        await expectAsync(myTaskResult).withOutput(async ({ contentType, value }) =\u003e {\n            expect(contentType)\n                // withContext give more information about of what you're testing\n                .withContext(myTaskResult.format('Body should be utf-8 JSON'))\n                .toEqual('application/json; charset=utf-8');\n\n            expect(value).toEqual({ hello: 'world' }, myTaskResult.format('Output body'));\n        });\n\n        // reads any key, fails the test if not found\n        await expectAsync(myTaskResult).withKeyValueStore(async ({ key, contentType, value }) =\u003e {\n            expect(value).toEqual({ status: true });\n        }, { keyName: 'INPUT' });\n\n        // gets requestQueue information\n        await expectAsync(myTaskResult).withRequestQueue(async ({\n            // contains everything from RequestQueueInfo\n            id, userId, createdAt,\n            modifiedAt, accessedAt, expireAt,\n            totalRequestCount, handledRequestCount, pendingRequestCount,\n            actId, actRunId, hadMultipleClients\n        }) =\u003e {\n            expect(totalRequestCount).toBeGreaterThan(1);\n        });\n\n        // check log for errors\n        await expectAsync(myTaskResult).withLog((log) =\u003e {\n            expect(log).not.toContain('ReferenceError');\n            expect(log).not.toContain('TypeError');\n            expect(log).not.toContain('The function passed to Apify.main() threw an exception');\n        });\n\n        // Check for dataset consistency\n        await expectAsync(myTaskResult).withChecker(({ runResult, output }) =\u003e {\n            expect(output.badItemCount).toBe(0);\n        }, {\n            functionalChecker: () =\u003e ({\n                myField: (field) =\u003e typeof field === 'string'\n            })\n        });\n\n        // Check for duplicate items\n        await expectAsync(myTaskResult).withDuplicates(({ runResult, output }) =\u003e {\n            expect(output).toEqual({});\n        }, {\n            taskId: 'myTaskId'\n        })\n    });\n\n  });\n}\n```\n\nSupports all extra Jasmine matchers, including asymmetrical matchers from https://github.com/JamieMason/Jasmine-Matchers\nTo access `any` without the JS editor complaining on the platform, you need to use `global.any[asymmetricMatcher]`\n\nThe special `run` parameter gives you the hability to run your tasks or actors, and return an accessor for their resources:\n\n```js\nconst result = await run({\n  taskId: 'xxx',  // task either by id or using user/task-name\n  actorId: 'xxx', // actor either by id or using user/actor-name\n  input: {}       // custom input override\n  options: {}     // specific memory, timeout options\n  nonce: '1'      // additional nonce for tasks running with the same input and options\n  name: 'run name'// give the run a name to be able to distinguish between them\n});\n```\n\nThe `run` is idempotent and will run the same tasks once per test, but you can specify the `nonce` to force running it everytime\n\nThe `run` function returns an object with standard API client run info with extra data:\n```js\nrunResult = {\n    runInput, // Actual input of the run with default fields filled\n    maxResults, // Attempts at parsing maxResults or similar field from input (use runInput to do this yourself)\n    data: {\n        ...runInfo,\n        taskId,\n        actorName,\n        taskName,\n        name: run.name,\n    }\n```\n\n## Matchers\n\nThose async matchers are lazy and only evaluated when you use them. You should use the result from `run` function to run `expectAsync()` on.\nThey abstract many common platform API calls. All callbacks can be plain closures or async ones, they are awaited anyway.\n\nYou also have full access to the Apify variable inside your tests.\n\n#### toHaveStatus(status: 'SUCCEEDED' | 'FAILED' | 'ABORTED' | 'TIMED-OUT')\nChecks for the proper run status\n\n#### withLog((logContent: string) =\u003e void)\nRun expectations on the `logContent`\n\n#### withDuplicates((result: { runResult: Object, output: Object }) =\u003e void, input?: Object)\nEnsures that no duplicates are found. You can provide a `taskId` with a pre-configured task or you can\nprovide all the input manually according to the docs [here](https://apify.com/lukaskrivka/duplications-checker/input-schema)\nBy default, anything above 2 counted items are considered duplicates\n\nReturns the `OUTPUT` of the run, containing an object like this:\n\n```jsonc\n{\n  // the keys here mean all the values that were found on the target dataset\n  \"$$\": {\n    \"count\": 4,\n    \"originalIndexes\": [\n      0,\n      12,\n      13,\n      15\n    ],\n    \"outputIndexes\": [\n      9,\n      10,\n      11,\n      13\n    ]\n  },\n  \"MISSING!\": { // this means it's missing or null value\n    \"count\": 8,\n    \"originalIndexes\": [\n      1,\n      3,\n      4,\n      6,\n      10,\n      14,\n      16,\n      17\n    ],\n    \"outputIndexes\": [\n      0,\n      1,\n      2,\n      5,\n      8,\n      12,\n      14,\n      15\n    ]\n  },\n  \"$$$\": {\n    \"count\": 4,\n    \"originalIndexes\": [\n      2,\n      5,\n      7,\n      8\n    ],\n    \"outputIndexes\": [\n      3,\n      4,\n      6,\n      7\n    ]\n  }\n}\n```\n\n#### withChecker((result: { runResult: Object, output: Object }) =\u003e void, input: Object, options?: Object)\n\nInput is required and you need at least a `taskId` parameter pointing to a\npre-configured results-checker task or you can pass everything to the input.\nCheck the docs [here](https://apify.com/lukaskrivka/results-checker/input-schema)\n\nOptions is the Apify.call/callTask options\nReturns the `OUTPUT` of the run, containing an object like this:\n\n```jsonc\n  \"totalItemCount\": 17,\n  \"badItemCount\": 0,\n  \"identificationFields\": [],\n  \"badFields\": {},\n  \"extraFields\": {},\n  \"totalFieldCounts\": {\n    \"categories\": 17,\n    \"info\": 17,\n    \"likes\": 17,\n    \"messenger\": 17,\n    \"posts\": 17,\n    \"priceRange\": 10,\n    \"title\": 17,\n    \"pageUrl\": 17,\n    \"address\": 17,\n    \"awards\": 17,\n    \"email\": 15,\n    \"impressum\": 17,\n    \"instagram\": 2,\n    \"phone\": 15,\n    \"products\": 17,\n    \"transit\": 4,\n    \"twitter\": 1,\n    \"website\": 16,\n    \"youtube\": 0,\n    \"mission\": 17,\n    \"overview\": 17,\n    \"payment\": 2,\n    \"checkins\": 12,\n    \"#startedAt\": 17,\n    \"verified\": 0,\n    \"#url\": 17,\n    \"#ref\": 17,\n    \"reviews\": 14,\n    \"#version\": 17,\n    \"#finishedAt\": 17\n  },\n  \"badItems\": \"https://api.apify.com/v2/key-value-stores/_/records/BAD-ITEMS?disableRedirect=true\"\n```\n\n#### withDataset((result: { dataset: Object, info: Object }) =\u003e void, options?: Object)\n\nReturns dataset information and the items. Options can be optionally passed to limit the number of items returned,\nusing `unwind` parameter, or any other option that is available here: [Dataset getItems](https://docs.apify.com/apify-client-js#ApifyClient-datasets-getItems)\n\nThe dataset object contains:\n\n```js\n{\n    items: [ [Object] ],\n    total: 1,\n    offset: 0,\n    count: 1,\n    limit: 999999999999\n}\n```\n\nThe info object contains:\n\n```js\n{\n    id: '',\n    userId: '',\n    createdAt: 2020-12-05T18:44:45.041Z,\n    modifiedAt: 2020-12-05T18:44:50.515Z,\n    accessedAt: 2020-12-05T18:44:50.515Z,\n    itemCount: 1,\n    cleanItemCount: 1,\n    actId: '',\n    actRunId: '',\n    stats: {\n      uploadedBytes: 0,\n      downloadedBytes: 0,\n      deflatedBytes: 0,\n      inflatedBytes: 21,\n      s3PutCount: 0,\n      s3GetCount: 0,\n      s3DeleteCount: 0,\n      readCount: 0,\n      writeCount: 1\n    }\n}\n```\n\nN.B.: this method waits at least 12 seconds to be able to read from the remote storage and make sure\nit's ready to be accessed after the task/actor has finished running using `run`\n\n#### withOutput((output: { value: any, contentType: string }) =\u003e void)\n\nReturns the `OUTPUT` key of the run. Can have any content type, check the contentType\n\n#### withStatistics((stats: Object) =\u003e void, options?: { index: number = 0 })\n\nReturns the `SDK_CRAWLER_STATISTICS_0` key of the run by default, unless provided with another index\nin the options.\n\nReturns an object like this:\n\n```json\n{\n  \"requestsFinished\": 217,\n  \"requestsFailed\": 99,\n  \"requestsRetries\": 0,\n  \"requestsFailedPerMinute\": 3,\n  \"requestsFinishedPerMinute\": 8,\n  \"requestMinDurationMillis\": 3071,\n  \"requestMaxDurationMillis\": 41800,\n  \"requestTotalFailedDurationMillis\": 686856,\n  \"requestTotalFinishedDurationMillis\": 3161769,\n  \"crawlerStartedAt\": \"2020-12-07T05:06:44.107Z\",\n  \"crawlerFinishedAt\": null,\n  \"statsPersistedAt\": \"2020-12-07T05:34:04.209Z\",\n  \"crawlerRuntimeMillis\": 1640402,\n  \"crawlerLastStartTimestamp\": 1607317603807,\n  \"requestRetryHistogram\": [\n    316\n  ],\n  \"statsId\": 0,\n  \"requestAvgFailedDurationMillis\": 6938,\n  \"requestAvgFinishedDurationMillis\": 14570,\n  \"requestTotalDurationMillis\": 3848625,\n  \"requestsTotal\": 316\n}\n```\n\n#### withKeyValueStore((output: { value: any, contentType: string }) =\u003e void, options: { keyName: string })\n\nReturns the content of the selected keyName. The test fails if the key doesn't exist.\nYou can access the INPUT that was used for the run using `{ keyName: 'INPUT' }`\n\n#### withRequestQueue((requestQueue: Object) =\u003e void)\n\nAccess the requestQueue object, that contains:\n\n```js\n{\n    id: '',\n    userId: '',\n    createdAt: 2020-12-05T18:44:45.048Z,\n    modifiedAt: 2020-12-05T18:44:45.048Z,\n    accessedAt: 2020-12-05T18:44:45.048Z,\n    expireAt: 2021-02-03T18:44:45.048Z,\n    totalRequestCount: 0,\n    handledRequestCount: 0,\n    pendingRequestCount: 0,\n    actId: '',\n    actRunId: '',\n    hadMultipleClients: false\n}\n```\n\nN.B.: all those exists only on `expectAsync` and need to be awaited, as demonstrated above:\n\n```js\nawait expectAsync(runResult).withDataset((something) =\u003e {\n    expect(something).toEqual('here');\n});\n```\n\n`jasmine.any()` and `jasmine.anything()` can be accessed using `global.jasmine`\n\n## Output\n\nThe tests output are available in the key value store under `OUTPUT` key, with the following structure:\n\n```json\n{\n  \"suite2\": {\n    \"id\": \"suite2\",\n    \"description\": \"one\",\n    \"fullName\": \"Actor tests one\",\n    \"failedExpectations\": [],\n    \"deprecationWarnings\": [],\n    \"duration\": 26484,\n    \"properties\": null,\n    \"status\": \"passed\",\n    \"specs\": [\n      {\n        \"id\": \"spec0\",\n        \"description\": \"should work\",\n        \"fullName\": \"Actor tests one should work\",\n        \"failedExpectations\": [],\n        \"passedExpectations\": [\n          {\n            \"matcherName\": \"toHaveStatus\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"toEqual\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"withDataset\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"withRequestQueue\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"withOutput\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"withKeyValueStore\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          },\n          {\n            \"matcherName\": \"withChecker\",\n            \"message\": \"Passed.\",\n            \"stack\": \"\",\n            \"passed\": true\n          }\n        ],\n        \"deprecationWarnings\": [],\n        \"pendingReason\": \"\",\n        \"duration\": 26480,\n        \"properties\": null,\n        \"status\": \"passed\"\n      }\n    ]\n  },\n  \"suite3\": {\n    \"id\": \"suite3\",\n    \"description\": \"two\",\n    \"fullName\": \"Actor tests two\",\n    \"failedExpectations\": [],\n    \"deprecationWarnings\": [],\n    \"duration\": 21,\n    \"properties\": null,\n    \"status\": \"passed\",\n    \"specs\": [\n      {\n        \"id\": \"spec1\",\n        \"description\": \"works\",\n        \"fullName\": \"Actor tests two works\",\n        \"failedExpectations\": [\n          {\n            \"matcherName\": \"toBe\",\n            \"message\": \"Expected true to be false.\",\n            \"stack\": \"Error: Expected true to be false.\\n    at \u003cJasmine\u003e\\n    at listOnTimeout (internal/timers.js:549:17)\\n    at processTimers (internal/timers.js:492:7)\",\n            \"passed\": false,\n            \"expected\": false,\n            \"actual\": true\n          }\n        ],\n        \"passedExpectations\": [],\n        \"deprecationWarnings\": [],\n        \"pendingReason\": \"\",\n        \"duration\": 15,\n        \"properties\": null,\n        \"status\": \"failed\"\n      }\n    ]\n  }\n}\n```\n\n## Expected consumption\n\nThis is a very lightweight actor that only intermediates actor runs, it can be run with the lowest amount of memory, which is 128MB.\nRunning for an hour should consume around 0.125 CUs.\n\n## Reasoning\n\nAutomated and integration tests are a must have for any complex piece of software. For Apify actors, it's no different.\nApify actors can be one (or many inputs) to one output, or it can have many items (through the dataset).\n\n## License\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpocesar%2Factor-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpocesar%2Factor-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpocesar%2Factor-testing/lists"}