├── .forceignore ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── create-package-version.yml │ ├── on-main-push.yml │ ├── on-pull-request.yml │ ├── on-release-published.yml │ ├── validate-namespace-compatibility.yml │ └── validate-package-version.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── apex-ruleset.xml ├── commitlint.config.js ├── config └── project-scratch-def.json ├── force-app ├── recipes │ └── classes │ │ ├── ApexMockeryOverview.cls │ │ ├── ApexMockeryOverview.cls-meta.xml │ │ ├── OperaPastryMatchable.cls │ │ ├── OperaPastryMatchable.cls-meta.xml │ │ ├── asserting │ │ ├── HasBeenCalled.cls │ │ ├── HasBeenCalled.cls-meta.xml │ │ ├── HasBeenCalledTimes.cls │ │ ├── HasBeenCalledTimes.cls-meta.xml │ │ ├── HasBeenCalledWith.cls │ │ ├── HasBeenCalledWith.cls-meta.xml │ │ ├── HasBeenCalledWithCustomMatchable.cls │ │ ├── HasBeenCalledWithCustomMatchable.cls-meta.xml │ │ ├── HasBeenCalledWithJSONMatchable.cls │ │ ├── HasBeenCalledWithJSONMatchable.cls-meta.xml │ │ ├── HasBeenCalledWithTypeMatchable.cls │ │ ├── HasBeenCalledWithTypeMatchable.cls-meta.xml │ │ ├── HasBeenLastCalledWith.cls │ │ ├── HasBeenLastCalledWith.cls-meta.xml │ │ ├── HasNotBeenCalled.cls │ │ └── HasNotBeenCalled.cls-meta.xml │ │ ├── mocking │ │ ├── Behave.cls │ │ ├── Behave.cls-meta.xml │ │ ├── BehaveOnce.cls │ │ ├── BehaveOnce.cls-meta.xml │ │ ├── NoConfiguration.cls │ │ ├── NoConfiguration.cls-meta.xml │ │ ├── Returns.cls │ │ ├── Returns.cls-meta.xml │ │ ├── ReturnsOnce.cls │ │ ├── ReturnsOnce.cls-meta.xml │ │ ├── ReturnsThenThrows.cls │ │ ├── ReturnsThenThrows.cls-meta.xml │ │ ├── Throws.cls │ │ ├── Throws.cls-meta.xml │ │ ├── ThrowsOnce.cls │ │ ├── ThrowsOnce.cls-meta.xml │ │ ├── ThrowsThenReturns.cls │ │ ├── ThrowsThenReturns.cls-meta.xml │ │ ├── WhenCalledWithCustomMatchable_ThenReturn.cls │ │ ├── WhenCalledWithCustomMatchable_ThenReturn.cls-meta.xml │ │ ├── WhenCalledWithEqualMatching_ThenReturn.cls │ │ ├── WhenCalledWithEqualMatching_ThenReturn.cls-meta.xml │ │ ├── WhenCalledWithJSONMatching_ThenReturn.cls │ │ ├── WhenCalledWithJSONMatching_ThenReturn.cls-meta.xml │ │ ├── WhenCalledWithMatchingThrowsAndReturns.cls │ │ ├── WhenCalledWithMatchingThrowsAndReturns.cls-meta.xml │ │ ├── WhenCalledWithNotMatchingAndReturn.cls │ │ ├── WhenCalledWithNotMatchingAndReturn.cls-meta.xml │ │ ├── WhenCalledWithTypeMatching_ThenReturn.cls │ │ ├── WhenCalledWithTypeMatching_ThenReturn.cls-meta.xml │ │ ├── WhenCalledWith_ThenBehave.cls │ │ ├── WhenCalledWith_ThenBehave.cls-meta.xml │ │ ├── WhenCalledWith_ThenReturnOnce.cls │ │ ├── WhenCalledWith_ThenReturnOnce.cls-meta.xml │ │ ├── WhenCalledWith_ThenThrow.cls │ │ ├── WhenCalledWith_ThenThrow.cls-meta.xml │ │ ├── WhenCalledWith_ThenThrowOnce.cls │ │ ├── WhenCalledWith_ThenThrowOnce.cls-meta.xml │ │ ├── WhenCalledWithoutMatchingConfiguration.cls │ │ └── WhenCalledWithoutMatchingConfiguration.cls-meta.xml │ │ └── setup │ │ ├── Bakery.cls │ │ ├── Bakery.cls-meta.xml │ │ ├── DeliveryService.cls │ │ ├── DeliveryService.cls-meta.xml │ │ ├── OrderConfirmation.cls │ │ ├── OrderConfirmation.cls-meta.xml │ │ ├── Pastry.cls │ │ ├── Pastry.cls-meta.xml │ │ ├── RecipeException.cls │ │ └── RecipeException.cls-meta.xml ├── src │ └── classes │ │ ├── Argument.cls │ │ ├── Argument.cls-meta.xml │ │ ├── Expect.cls │ │ ├── Expect.cls-meta.xml │ │ ├── MethodSpy.cls │ │ ├── MethodSpy.cls-meta.xml │ │ ├── Mock.cls │ │ └── Mock.cls-meta.xml └── test │ ├── namespace │ └── classes │ │ ├── ApexMockeryOverview.cls │ │ ├── ApexMockeryOverview.cls-meta.xml │ │ └── utils │ │ ├── StubBuilderImpl.cls │ │ └── StubBuilderImpl.cls-meta.xml │ └── package │ └── classes │ ├── functional │ ├── BusinessService.cls │ ├── BusinessService.cls-meta.xml │ ├── DMLDelegate.cls │ ├── DMLDelegate.cls-meta.xml │ ├── FunctionalTest.cls │ └── FunctionalTest.cls-meta.xml │ └── unit │ ├── ArgumentTest.cls │ ├── ArgumentTest.cls-meta.xml │ ├── DummyInterface.cls │ ├── DummyInterface.cls-meta.xml │ ├── ExpectTest.cls │ ├── ExpectTest.cls-meta.xml │ ├── MethodSpyOnceTest.cls │ ├── MethodSpyOnceTest.cls-meta.xml │ ├── MethodSpyTest.cls │ ├── MethodSpyTest.cls-meta.xml │ ├── MethodSpyTestCustomBehavior.cls │ ├── MethodSpyTestCustomBehavior.cls-meta.xml │ ├── MethodSpyTimesTest.cls │ ├── MethodSpyTimesTest.cls-meta.xml │ ├── MockTest.cls │ ├── MockTest.cls-meta.xml │ └── deprecated │ ├── DeprecatedMethodSpyTest.cls │ └── DeprecatedMethodSpyTest.cls-meta.xml ├── package-lock.json ├── package.json ├── postInstall.sh ├── resources ├── class_diagram.png └── logo.png └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## _Who_ is the bug affecting? 2 | 3 | 4 | 5 | ## _What_ is affected by this bug? 6 | 7 | 8 | 9 | ## _When_ does this occur? 10 | 11 | 12 | 13 | ## _Where_ on the platform does it happen? 14 | 15 | 16 | 17 | ## _How_ do we replicate the issue? 18 | 19 | 20 | 21 | ## Expected behavior (i.e. solution) 22 | 23 | 24 | 25 | ## Other Comments 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Motivation and Context 8 | 9 | 10 | 11 | 12 | ## How Has This Been Tested? 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | cache: "npm" 16 | 17 | - name: Set environment variables 18 | run: | 19 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 20 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 21 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 22 | 23 | - name: Install sfdx and connect to DevHub 24 | run: | 25 | npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 26 | echo ${{ secrets.DEVHUB_URL }} > sfdx_auth 27 | sf org login sfdx-url --sfdx-url-file sfdx_auth --set-default-dev-hub --alias hub 28 | sf config set target-dev-hub=hub --global 29 | 30 | - name: Create scratch org 31 | run: sf org create scratch --no-namespace --definition-file config/project-scratch-def.json --alias scratch-org --set-default --duration-days 1 32 | 33 | - name: Install npm dependencies 34 | run: npm install 35 | 36 | - name: Push source to scratch org 37 | run: npm run build 38 | 39 | - name: Run Apex tests 40 | run: npm run test 41 | 42 | - name: Delete scratch org 43 | if: always() 44 | run: sf org delete scratch --no-prompt 45 | -------------------------------------------------------------------------------- /.github/workflows/create-package-version.yml: -------------------------------------------------------------------------------- 1 | name: create package version 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | type: string 7 | default: ${{ github.head_ref }} 8 | packageAlias: 9 | required: true 10 | type: string 11 | outputs: 12 | packageId: 13 | description: 04t package version id created 14 | value: ${{ jobs.create-package-version.outputs.packageId }} 15 | 16 | jobs: 17 | create-package-version: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | packageId: ${{ steps.create.outputs.packageId }} 21 | steps: 22 | - name: Set environment variables 23 | run: | 24 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 25 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 26 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 27 | echo "SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_VERSION_CREATE=true" >> $GITHUB_ENV 28 | 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | cache: 'npm' 36 | 37 | - name: Install sfdx and connect to DevHub 38 | run: | 39 | npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 40 | echo ${{ secrets.DEVHUB_URL }} > sfdx_auth 41 | sf org login sfdx-url --sfdx-url-file sfdx_auth --set-default-dev-hub --alias hub 42 | sf config set target-dev-hub=hub --global 43 | 44 | - name: Create package version 45 | id: create 46 | run: | 47 | packageId=$(sf package version create --definition-file config/project-scratch-def.json --package "${{ inputs.packageAlias }}" --branch "${{ inputs.branch }}" --tag ${{ github.sha }} --wait 120 --code-coverage --skip-ancestor-check --installation-key-bypass --json | jq -e -r ".result.SubscriberPackageVersionId") 48 | echo "packageId=$packageId" >> $GITHUB_ENV 49 | echo "packageId=$packageId" >> $GITHUB_OUTPUT 50 | -------------------------------------------------------------------------------- /.github/workflows/on-main-push.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags-ignore: 7 | - '**' 8 | 9 | jobs: 10 | integrate: 11 | uses: ./.github/workflows/build.yml 12 | secrets: inherit 13 | 14 | create-namespaced-package-version: 15 | uses: ./.github/workflows/create-package-version.yml 16 | needs: integrate 17 | with: 18 | packageAlias: Apex Mockery NS Test 19 | secrets: inherit 20 | 21 | validate-namespace-compatibility: 22 | uses: ./.github/workflows/validate-namespace-compatibility.yml 23 | needs: create-namespaced-package-version 24 | with: 25 | packageId: ${{ needs.create-namespaced-package-version.outputs.packageId }} 26 | secrets: inherit 27 | 28 | create-package-version: 29 | uses: ./.github/workflows/create-package-version.yml 30 | needs: integrate 31 | with: 32 | packageAlias: Apex Mockery 33 | secrets: inherit 34 | 35 | validate-package-version: 36 | uses: ./.github/workflows/validate-package-version.yml 37 | needs: create-package-version 38 | with: 39 | packageId: ${{ needs.create-package-version.outputs.packageId }} 40 | secrets: inherit 41 | 42 | prepare-release: 43 | needs: [validate-package-version, validate-namespace-compatibility] 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: google-github-actions/release-please-action@v4 47 | with: 48 | token: ${{ secrets.RELEASE_TOKEN }} 49 | release-type: sfdx 50 | package-name: apex-mockery -------------------------------------------------------------------------------- /.github/workflows/on-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: integrate 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths-ignore: 7 | - "resources/**" 8 | - "*.md" 9 | - "CODEOWNERS" 10 | - "package.json" 11 | - "sfdx-project.json" 12 | - "config/**" 13 | 14 | # Manage concurrency to stop running jobs and start new ones in case of new commit pushed 15 | concurrency: 16 | group: ${{ github.ref }}-${{ github.workflow }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | formatting-and-linting: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout source code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: 20 31 | cache: 'npm' 32 | 33 | - name: Set environment variables 34 | run: | 35 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 36 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 37 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 38 | 39 | - name: Install SFDX CLI 40 | run: npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 41 | 42 | - name: Install npm dependencies 43 | run: npm install 44 | 45 | - name: Code formatting verification with Prettier 46 | run: npm run prettier:verify 47 | 48 | - name: Apex static analysis 49 | run: npm run lint:apex 50 | 51 | 52 | - name: Check documentation writing 53 | run: npm run lint:doc 54 | 55 | integrate: 56 | uses: ./.github/workflows/build.yml 57 | secrets: inherit 58 | 59 | create-namespaced-package-version: 60 | uses: ./.github/workflows/create-package-version.yml 61 | needs: [integrate, formatting-and-linting] 62 | with: 63 | packageAlias: Apex Mockery NS Test 64 | secrets: inherit 65 | 66 | validate-namespace-compatibility: 67 | uses: ./.github/workflows/validate-namespace-compatibility.yml 68 | needs: create-namespaced-package-version 69 | with: 70 | packageId: ${{ needs.create-namespaced-package-version.outputs.packageId }} 71 | secrets: inherit 72 | 73 | create-package-version: 74 | uses: ./.github/workflows/create-package-version.yml 75 | needs: [integrate, formatting-and-linting] 76 | with: 77 | packageAlias: Apex Mockery 78 | secrets: inherit 79 | 80 | validate-package-version: 81 | uses: ./.github/workflows/validate-package-version.yml 82 | needs: create-package-version 83 | with: 84 | packageId: ${{ needs.create-package-version.outputs.packageId }} 85 | secrets: inherit 86 | 87 | commit-lint: 88 | runs-on: ubuntu-latest 89 | steps: 90 | - uses: actions/checkout@v4 91 | with: 92 | fetch-depth: 0 93 | - uses: wagoid/commitlint-github-action@v6 94 | continue-on-error: true 95 | 96 | pr-lint: 97 | permissions: 98 | pull-requests: read 99 | statuses: write 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: amannn/action-semantic-pull-request@v5 103 | env: 104 | GITHUB_TOKEN: ${{ github.token }} 105 | -------------------------------------------------------------------------------- /.github/workflows/on-release-published.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | create-namespaced-package-version: 9 | uses: ./.github/workflows/create-package-version.yml 10 | with: 11 | branch: release 12 | packageAlias: Apex Mockery NS Test 13 | secrets: inherit 14 | 15 | validate-namespace-compatibility: 16 | uses: ./.github/workflows/validate-namespace-compatibility.yml 17 | needs: create-namespaced-package-version 18 | with: 19 | packageId: ${{ needs.create-namespaced-package-version.outputs.packageId }} 20 | secrets: inherit 21 | 22 | create-package-version: 23 | uses: ./.github/workflows/create-package-version.yml 24 | with: 25 | branch: release 26 | packageAlias: Apex Mockery 27 | secrets: inherit 28 | 29 | validate-package-version: 30 | uses: ./.github/workflows/validate-package-version.yml 31 | needs: create-package-version 32 | with: 33 | packageId: ${{ needs.create-package-version.outputs.packageId }} 34 | secrets: inherit 35 | 36 | promote-package-version: 37 | runs-on: ubuntu-latest 38 | needs: [create-package-version, validate-package-version, validate-namespace-compatibility] 39 | steps: 40 | - name: Set environment variables 41 | run: | 42 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 43 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 44 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 45 | echo "SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_VERSION_CREATE=true" >> $GITHUB_ENV 46 | 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - uses: actions/setup-node@v4 51 | with: 52 | node-version: 20 53 | cache: 'npm' 54 | 55 | - name: Install sfdx and connect to DevHub 56 | run: | 57 | npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 58 | echo ${{ secrets.DEVHUB_URL }} > sfdx_auth 59 | sf org login sfdx-url --sfdx-url-file sfdx_auth --set-default-dev-hub --alias hub 60 | sf config set target-dev-hub=hub --global 61 | 62 | - name: Promote latest main package version 63 | run: sf package version promote --no-prompt --package ${{ needs.create-package-version.outputs.packageId }} 64 | 65 | update-release: 66 | runs-on: ubuntu-latest 67 | needs: [create-package-version, promote-package-version] 68 | steps: 69 | - name: Create package links 70 | run: | 71 | packageId=${{ needs.create-package-version.outputs.packageId }} 72 | links=$(cat << EOF 73 | ### Unlocked Package installation 74 | - Production [link](https://login.salesforce.com/packaging/installPackage.apexp?p0=$packageId) 75 | - Sandbox [link](https://test.salesforce.com/packaging/installPackage.apexp?p0=$packageId) 76 | - sfdx command: \`sf package install --package $packageId\` 77 | EOF 78 | ) 79 | echo "links<> $GITHUB_ENV 80 | echo "$links" >> $GITHUB_ENV 81 | echo "EOF" >> $GITHUB_ENV 82 | 83 | - name: Update release with package Id 84 | uses: irongut/EditRelease@v1.2.0 85 | with: 86 | token: ${{ github.token }} 87 | id: ${{ github.event.release.id }} 88 | body: ${{ env.links }} 89 | replacebody: false 90 | 91 | - name: Communicate release information 92 | uses: apexskier/github-release-commenter@v1 93 | with: 94 | GITHUB_TOKEN: ${{ github.token }} 95 | comment-template: | 96 | Shipped in [release `{release_tag}`]({release_link}). 97 | ${{ env.links }} 98 | -------------------------------------------------------------------------------- /.github/workflows/validate-namespace-compatibility.yml: -------------------------------------------------------------------------------- 1 | name: validate namespace compatibility 2 | on: 3 | workflow_call: 4 | inputs: 5 | packageId: 6 | required: true 7 | description: 04t package version id to validate 8 | type: string 9 | 10 | jobs: 11 | validate-namespace-compatibility: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set environment variables 15 | run: | 16 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 17 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 18 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 19 | echo "SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_VERSION_CREATE=true" >> $GITHUB_ENV 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'npm' 28 | 29 | - name: Install sfdx and connect to DevHub 30 | run: | 31 | npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 32 | echo ${{ secrets.DEVHUB_URL }} > sfdx_auth 33 | sf org login sfdx-url --sfdx-url-file sfdx_auth --set-default-dev-hub --alias hub 34 | sf config set target-dev-hub=hub --global 35 | 36 | - name: Create scratch org 37 | run: sf org create scratch --definition-file config/project-scratch-def.json --alias scratch-org --set-default --duration-days 1 38 | 39 | - name: Check package installation 40 | run: sf package install --package ${{ inputs.packageId }} --wait 10 41 | 42 | - name: Deploy cross namespace tests 43 | run: | 44 | sf project deploy start --source-dir force-app/recipes/classes/setup 45 | sf project deploy start --source-dir force-app/test/namespace 46 | 47 | - name: Execute tests 48 | run: sf apex run test --result-format human --output-dir ./tests/apex --test-level RunLocalTests --wait 20 49 | 50 | - name: Delete scratch org 51 | if: always() 52 | run: sf org delete scratch --no-prompt 53 | -------------------------------------------------------------------------------- /.github/workflows/validate-package-version.yml: -------------------------------------------------------------------------------- 1 | name: validate package version 2 | on: 3 | workflow_call: 4 | inputs: 5 | packageId: 6 | required: true 7 | description: 04t package version id to validate 8 | type: string 9 | 10 | jobs: 11 | validate-package-version: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set environment variables 15 | run: | 16 | echo "SF_DISABLE_AUTOUPDATE=true" >> $GITHUB_ENV 17 | echo "SF_DISABLE_SOURCE_MEMBER_POLLING=true" >> $GITHUB_ENV 18 | echo "SF_DISABLE_TELEMETRY=true" >> $GITHUB_ENV 19 | echo "SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_VERSION_CREATE=true" >> $GITHUB_ENV 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'npm' 28 | 29 | - name: Install sfdx and connect to DevHub 30 | run: | 31 | npm install --global @salesforce/cli@${{ vars.SFDX_VERSION }} 32 | echo ${{ secrets.DEVHUB_URL }} > sfdx_auth 33 | sf org login sfdx-url --sfdx-url-file sfdx_auth --set-default-dev-hub --alias hub 34 | sf config set target-dev-hub=hub --global 35 | 36 | - name: Create scratch org 37 | run: sf org create scratch --no-namespace --definition-file config/project-scratch-def.json --alias scratch-org --set-default --duration-days 1 38 | 39 | - name: Check package installation 40 | run: sf package install --package ${{ inputs.packageId }} --wait 10 41 | 42 | - name: Deploy recipes 43 | run: sf project deploy start --source-dir force-app/recipes 44 | 45 | - name: Execute tests 46 | run: sf apex run test --result-format human --output-dir ./tests/apex --test-level RunLocalTests --wait 20 47 | 48 | - name: Delete scratch org 49 | if: always() 50 | run: sf org delete scratch --no-prompt 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sf/ 7 | .sfdx/ 8 | .vscode/ 9 | .localdevserver/ 10 | deploy-options.json 11 | 12 | # SFDX apex test 13 | tests 14 | 15 | # LWC VSCode autocomplete 16 | **/lwc/jsconfig.json 17 | 18 | # LWC Jest coverage reports 19 | coverage/ 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Dependency directories 29 | node_modules/ 30 | .wireit/ 31 | 32 | # Eslint cache 33 | .eslintcache 34 | 35 | # MacOS system files 36 | .DS_Store 37 | 38 | # Windows system files 39 | Thumbs.db 40 | ehthumbs.db 41 | [Dd]esktop.ini 42 | $RECYCLE.BIN/ 43 | 44 | # Local environment variables 45 | .env 46 | 47 | # SFDX 48 | tests/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx commitlint --edit -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run precommit -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run prepush -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sf 8 | .sfdx 9 | .vscode 10 | .github 11 | coverage 12 | tests 13 | CHANGELOG.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "printWidth": 160, 5 | "semi": true, 6 | "overrides": [ 7 | { 8 | "files": "**/lwc/**/*.html", 9 | "options": { "parser": "lwc" } 10 | }, 11 | { 12 | "files": "*.{cmp,page,component}", 13 | "options": { "parser": "html" } 14 | } 15 | ], 16 | "plugins": ["prettier-plugin-apex", "@prettier/plugin-xml"] 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.3.0](https://github.com/salesforce/apex-mockery/compare/v2.2.0...v2.3.0) (2025-01-31) 4 | 5 | 6 | ### Features 7 | 8 | * implement custom implementation behavior configuration ([#84](https://github.com/salesforce/apex-mockery/issues/84)) ([d8a4d53](https://github.com/salesforce/apex-mockery/commit/d8a4d531740f3a9ce22bd55d333cd6bb1e6555d3)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * case for IsTest and Datetime ([#86](https://github.com/salesforce/apex-mockery/issues/86)) ([92817cc](https://github.com/salesforce/apex-mockery/commit/92817cc972c805a4fb810544b71093bcc2e3bac9)) 14 | 15 | ## [2.2.0](https://github.com/salesforce/apex-mockery/compare/v2.1.0...v2.2.0) (2024-11-29) 16 | 17 | 18 | ### Features 19 | 20 | * add "global" and "matching" `once` API ([#69](https://github.com/salesforce/apex-mockery/issues/69)) ([f5bb8d9](https://github.com/salesforce/apex-mockery/commit/f5bb8d936932d11ef7bd425a591737ca4a194717)) 21 | * add "global" and "matching" `times` API ([#72](https://github.com/salesforce/apex-mockery/issues/72)) ([6e13d6a](https://github.com/salesforce/apex-mockery/commit/6e13d6a6281ef17ce224f65f1fe58427cdc2791b)) 22 | * add names and types of method parameters in error output ([#62](https://github.com/salesforce/apex-mockery/issues/62)) ([4c6cc01](https://github.com/salesforce/apex-mockery/commit/4c6cc012b1a90c8b756f08d148a454a25642c84e)) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * case insensitive matching for mock ([#75](https://github.com/salesforce/apex-mockery/issues/75)) ([37f958e](https://github.com/salesforce/apex-mockery/commit/37f958e3872227b1099bf284a717fb322a839eda)) 28 | * issue when punctual matchers are consumed ([#77](https://github.com/salesforce/apex-mockery/issues/77)) ([a07d6a1](https://github.com/salesforce/apex-mockery/commit/a07d6a11cfe2448df6783fee36748f33c358605a)) 29 | * spy calls with newer SObjects (and misc. updates) ([#63](https://github.com/salesforce/apex-mockery/issues/63)) ([d914655](https://github.com/salesforce/apex-mockery/commit/d91465551026e4034fa7c75fc2ec243c4c595dd1)) 30 | 31 | ## [2.1.0](https://github.com/salesforce/apex-mockery/compare/v2.0.0...v2.1.0) (2023-05-12) 32 | 33 | 34 | ### Features 35 | 36 | * Improve naming and concept segregation ([#50](https://github.com/salesforce/apex-mockery/issues/50)) ([4e35e09](https://github.com/salesforce/apex-mockery/commit/4e35e09380af8ea8a34462a519a2c6b64bcb4fc2)) 37 | 38 | ## [2.0.0](https://github.com/salesforce/apex-mockery/compare/v1.1.0...v2.0.0) (2023-05-05) 39 | 40 | 41 | ### Features 42 | 43 | * migrate from `parameter` concept to `argument` ([#45](https://github.com/salesforce/apex-mockery/issues/45)) ([ec16b56](https://github.com/salesforce/apex-mockery/commit/ec16b567cbfffc6391f7ad3d8936b3902f075774)) 44 | 45 | ### Refactorings 46 | 47 | * rename `Assertions.assertThat` in `Expect.that` ([#47](https://github.com/salesforce/apex-mockery/issues/47)) ([7d74eb8](https://github.com/salesforce/apex-mockery/commit/7d74eb8a7644ef181dcb83eeb1811397cf3d0ac4)) 48 | 49 | ## [1.1.0](https://github.com/salesforce/apex-mockery/compare/v1.0.0...v1.1.0) (2023-03-09) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * not usable unlocked package without namespace ([#39](https://github.com/salesforce/apex-mockery/issues/39)) ([41c71df](https://github.com/salesforce/apex-mockery/commit/41c71dffbb61e4fa0a83c04fa740ff1590be890c)) 55 | 56 | ## 1.0.0 (2023-02-22) 57 | 58 | 59 | ### Features 60 | 61 | * add auto release communication bot ([#30](https://github.com/salesforce/apex-mockery/issues/30)) ([5cbdb63](https://github.com/salesforce/apex-mockery/commit/5cbdb6316220fb8e5fa05500724186d62f16f1a6)) 62 | * add call log history to concerned error messages ([2e2a460](https://github.com/salesforce/apex-mockery/commit/2e2a4602ff232c6f8ef8adaac03f23cdc6d8747a)) 63 | * add hasbeenCalledWith and hasBeenLastCalledWith ([#8](https://github.com/salesforce/apex-mockery/issues/8)) ([98ebbf2](https://github.com/salesforce/apex-mockery/commit/98ebbf26dd701ca168e19c50ef3560ca8033b51b)) 64 | * add recipes ([#22](https://github.com/salesforce/apex-mockery/issues/22)) ([745fed6](https://github.com/salesforce/apex-mockery/commit/745fed6566c82e5d9536298178bc17f1366ff460)) 65 | * add test classes ([#4](https://github.com/salesforce/apex-mockery/issues/4)) ([770b00f](https://github.com/salesforce/apex-mockery/commit/770b00f2adabcbf270f2004ebe621e19417e9672)) 66 | * add thenThrow API to spy ([#7](https://github.com/salesforce/apex-mockery/issues/7)) ([ff32fbf](https://github.com/salesforce/apex-mockery/commit/ff32fbffe21110f1fbc791f9abdfaa201ace3fdb)) 67 | * enhance assertion error messages by including call history ([#17](https://github.com/salesforce/apex-mockery/issues/17)) ([2e2a460](https://github.com/salesforce/apex-mockery/commit/2e2a4602ff232c6f8ef8adaac03f23cdc6d8747a)) 68 | * implement whenCalledWithParams using ParameterMatcher ([#10](https://github.com/salesforce/apex-mockery/issues/10)) ([127359c](https://github.com/salesforce/apex-mockery/commit/127359c417a9ac4d164451588f720caa0970bbf9)) 69 | * improve configuration error messages ([#21](https://github.com/salesforce/apex-mockery/issues/21)) ([3670ce8](https://github.com/salesforce/apex-mockery/commit/3670ce85e8ea602bad9d4d0ec56c0b31b48aebcb)) 70 | * improve existing functional test ([#11](https://github.com/salesforce/apex-mockery/issues/11)) ([f2b5760](https://github.com/salesforce/apex-mockery/commit/f2b576072e26278c019344df3fdd16e22ea55874)) 71 | * improve README content ([#2](https://github.com/salesforce/apex-mockery/issues/2)) ([64dd0fc](https://github.com/salesforce/apex-mockery/commit/64dd0fc4dbb4c6c90e24a29f195deb0795a8df59)) 72 | * introduce convenient API to avoid Params usage ([#25](https://github.com/salesforce/apex-mockery/issues/25)) ([6d9efa4](https://github.com/salesforce/apex-mockery/commit/6d9efa4af9eea1a91fcaf3dc1a1540ec009f0422)) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * enhance docs and samples readability ([#24](https://github.com/salesforce/apex-mockery/issues/24)) ([9fe4611](https://github.com/salesforce/apex-mockery/commit/9fe461107587f5144d4f1bc47103119f46676ec7)) 78 | * small cleanup ([#26](https://github.com/salesforce/apex-mockery/issues/26)) ([30aede9](https://github.com/salesforce/apex-mockery/commit/30aede9aa7aaa0e7c779d1cea505dfa07c62de63)) 79 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 2 | #ECCN:Open Source 3 | * @scolladon @LudoMeurillon -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | - Using welcoming and inclusive language 39 | - Being respectful of differing viewpoints and experiences 40 | - Gracefully accepting constructive criticism 41 | - Focusing on what is best for the community 42 | - Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | - The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | - Personal attacks, insulting/derogatory comments, or trolling 49 | - Public or private harassment 50 | - Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | - Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | - Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org "https://www.contributor-covenant.org/" 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to apex-mockery 2 | 3 | We encourage the developer community to contribute to this repository. This guide has instructions to install, build, test and contribute to the framework. 4 | 5 | - [Requirements](#requirements) 6 | - [Installation](#installation) 7 | - [Testing](#testing) 8 | - [Git Workflow](#git-workflow) 9 | 10 | ## Requirements 11 | 12 | - [Node](https://nodejs.org/) >= 14 13 | 14 | ## Installation 15 | 16 | ### 1) Download the repository 17 | 18 | ```bash 19 | git clone git@github.com:salesforce/apex-mockery.git 20 | ``` 21 | 22 | ### 2) Install Dependencies 23 | 24 | This will install all the tools needed to contribute 25 | 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | ### 3) Build application 31 | 32 | ```bash 33 | sf project deploy start 34 | ``` 35 | 36 | Rebuild every time you made a change in the source and you need to test locally 37 | 38 | ## Testing 39 | 40 | ### Unit Testing 41 | 42 | When developing, use apex unit testing to provide test coverage for new functionality. To run the apex tests use the following command from the root directory: 43 | 44 | ```bash 45 | # just run test 46 | npm run test 47 | ``` 48 | 49 | To execute a particular test, use the sfdx command directly 50 | 51 | ## Editor Configurations 52 | 53 | Configure your editor to use our lint and code style rules. 54 | 55 | ### Code formatting 56 | 57 | [Prettier](https://prettier.io/) is a code formatter used to ensure consistent formatting across your code base. To use Prettier with Visual Studio Code, install [this extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) from the Visual Studio Code Marketplace. 58 | This repository provide [.prettierignore](/.prettierignore) and [.prettierrc](/.prettierrc.json) files to control the behaviour of the Prettier formatter. 59 | 60 | ### Code linting 61 | 62 | We use `sfdx-scanner` to execute code static analysis 63 | It is automatically installed for you via `npm install` 64 | 65 | ## Git Workflow 66 | 67 | The process of submitting a pull request is straightforward and 68 | generally follows the same pattern each time: 69 | 70 | 1. [Fork the repo](#fork-the-repo) 71 | 1. [Create a feature branch](#create-a-feature-branch) 72 | 1. [Make your changes](#make-your-changes) 73 | 1. [Rebase](#rebase) 74 | 1. [Check your submission](#check-your-submission) 75 | 1. [Create a pull request](#create-a-pull-request) 76 | 1. [Update the pull request](#update-the-pull-request) 77 | 78 | ### Fork the repo 79 | 80 | [Fork][fork-a-repo] the [salesforce/apex-mockery](https://github.com/salesforce/apex-mockery) repo. Clone your fork in your local workspace and [configure][configuring-a-remote-for-a-fork] your remote repository settings. 81 | 82 | ```bash 83 | git clone git@github.com:/apex-mockery.git 84 | cd apex-mockery 85 | git remote add upstream git@github.com:salesforce/apex-mockery.git 86 | ``` 87 | 88 | ### Create a feature branch 89 | 90 | ```bash 91 | git checkout main 92 | git pull origin main 93 | git checkout -b feature/ 94 | ``` 95 | 96 | ### Make your changes 97 | 98 | Change the files, build, test, lint and commit your code using the following command: 99 | 100 | ```bash 101 | git add 102 | git commit ... 103 | git push origin feature/ 104 | ``` 105 | 106 | Commit your changes using a descriptive commit message 107 | 108 | The above commands will commit the files into your feature branch. You can keep 109 | pushing new changes into the same branch until you are ready to create a pull 110 | request. 111 | 112 | ### Rebase 113 | 114 | Sometimes your feature branch will get stale on the main branch, 115 | and it will must a rebase. Do not use the github UI rebase to keep your commits signed. The following steps can help: 116 | 117 | ```bash 118 | git checkout main 119 | git pull upstream main 120 | git checkout feature/ 121 | git rebase upstream/main 122 | ``` 123 | 124 | _note: If no conflicts arise, these commands will apply your changes on top of the main branch. Resolve any conflicts._ 125 | 126 | ### Check your submission 127 | 128 | #### Lint your changes 129 | 130 | ```bash 131 | npm run lint 132 | ``` 133 | 134 | The above command may display lint issues not related to your changes. 135 | The recommended way to avoid lint issues is to [configure your 136 | editor][eslint-integrations] to warn you in real time as you edit the file. 137 | 138 | Fixing all existing lint issues is a tedious task so please pitch in by fixing 139 | the ones related to the files you make changes to! 140 | 141 | #### Run tests 142 | 143 | Test your change by running the unit tests and integration tests. Instructions [here](#testing). 144 | 145 | ### Create a pull request 146 | 147 | If you've never created a pull request before, follow [these 148 | instructions][creating-a-pull-request]. Pull request samples [here](https://github.com/salesforce/apex-mockery/pulls) 149 | 150 | ### Update the pull request 151 | 152 | ```sh 153 | git fetch origin 154 | git rebase origin/${base_branch} 155 | 156 | # Then force push it 157 | git push origin ${feature_branch} --force-with-lease 158 | ``` 159 | 160 | _note: If your pull request needs more changes, keep working on your feature branch as described above._ 161 | 162 | CI validates prettifying, linting and tests 163 | 164 | [fork-a-repo]: https://help.github.com/en/articles/fork-a-repo 165 | [configuring-a-remote-for-a-fork]: https://help.github.com/en/articles/configuring-a-remote-for-a-fork 166 | [setup-github-ssh]: https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/ 167 | [creating-a-pull-request]: https://help.github.com/articles/creating-a-pull-request/ 168 | [eslint-integrations]: http://eslint.org/docs/user-guide/integrations 169 | 170 | ### Collaborate on the pull request 171 | 172 | We use [Conventional Comments](https://conventionalcomments.org/) to ensure every comment expresses the intention and is easy to understand. 173 | Pull Request comments are not enforced, it is more a way to help the reviewers and contributors to collaborate on the pull request. 174 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Salesforce.com, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /apex-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | Custom Ruleset to use with Apex PMD 9 | 10 | 11 | 12 | 2 13 | 14 | 18 | 3 19 | 20 | 21 | 3 22 | 23 | 24 | 25 | 26 | 3 27 | 28 | 29 | 3 30 | 31 | 32 | 3 33 | 34 | 35 | 3 36 | 37 | 38 | 2 39 | 40 | 41 | 3 42 | 43 | 44 | 3 45 | 46 | 47 | 48 | 49 | 3 50 | 51 | 52 | 53 | 54 | 55 | 3 56 | 57 | 58 | 59 | 60 | 61 | 62 | 3 63 | 64 | 65 | 66 | 67 | 68 | 3 69 | 70 | 71 | 72 | 73 | 74 | 3 75 | 76 | 77 | 78 | 79 | 80 | 3 81 | 82 | 83 | 84 | 85 | 86 | 3 87 | 88 | 89 | 90 | 91 | 92 | 3 93 | 94 | 95 | 96 | 97 | 98 | 3 99 | 100 | 101 | 102 | 103 | 104 | 3 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 2 113 | 114 | 115 | 3 116 | 117 | 118 | 3 119 | 120 | 121 | 3 122 | 123 | 124 | 3 125 | 126 | 127 | 3 128 | 129 | 133 | 2 134 | 135 | 136 | 137 | 138 | 1 139 | 140 | 141 | 4 142 | 143 | 144 | 1 145 | 146 | 147 | 1 148 | 149 | 150 | 1 151 | 152 | 153 | 2 154 | 155 | 156 | 1 157 | 158 | 159 | 1 160 | 161 | 162 | 3 163 | 164 | 165 | 1 166 | 167 | 168 | 1 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "apex-mockery", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /force-app/recipes/classes/ApexMockeryOverview.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ApexMockeryOverview { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Chocolatine')).thenReturn(Date.today().addDays(3)).thenReturnOnce(Date.today().addDays(2)); 16 | planDeliverySpy.whenCalledWith(new OperaPastryMatchable()).thenThrow(new RecipeException()); 17 | planDeliverySpy.returns(Date.today().addDays(4)); 18 | 19 | Expect.that(planDeliverySpy).hasNotBeenCalled(); 20 | Expect.that(planDeliverySpy).hasBeenCalledTimes(0); 21 | 22 | // Act & Assert 23 | // Once matcher (imagine first command is delivered early) 24 | OrderConfirmation order = myBakery.order(new Pastry('Chocolatine')); 25 | Expect.that(planDeliverySpy).hasBeenCalled(); 26 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 27 | Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Chocolatine')); 28 | Expect.that(planDeliverySpy).hasBeenCalledTimes(1); 29 | Assert.areEqual(Date.today().addDays(2), order.deliveryDate); 30 | 31 | // Matcher 32 | order = myBakery.order(new Pastry('Chocolatine')); 33 | Expect.that(planDeliverySpy).hasBeenCalled(); 34 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 35 | Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Chocolatine')); 36 | Expect.that(planDeliverySpy).hasBeenCalledTimes(2); 37 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 38 | 39 | order = myBakery.order(new Pastry('Croissant')); 40 | Expect.that(planDeliverySpy).hasBeenCalled(); 41 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 42 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 43 | Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Croissant')); 44 | Expect.that(planDeliverySpy).hasBeenCalledTimes(3); 45 | Assert.areEqual(Date.today().addDays(4), order.deliveryDate); 46 | 47 | try { 48 | order = myBakery.order(new Pastry('Opera')); 49 | Assert.fail('Expected exception was not thrown'); 50 | } catch (Exception ex) { 51 | Assert.isInstanceOfType(ex, RecipeException.class); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /force-app/recipes/classes/ApexMockeryOverview.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/OperaPastryMatchable.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | public class OperaPastryMatchable implements Argument.Matchable { 9 | public Boolean matches(Object callArgument) { 10 | Pastry p = (Pastry) callArgument; 11 | return p.name == 'Opera'; 12 | } 13 | 14 | public Boolean equals(Object obj) { 15 | return obj != null && obj instanceof OperaPastryMatchable; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /force-app/recipes/classes/OperaPastryMatchable.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalled.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalled { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | 19 | // Assert 20 | Expect.that(planDeliverySpy).hasBeenCalled(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalled.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledTimes.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalledTimes { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenCalledTimes(2); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledTimes.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWith.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalledWith { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 22 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWith.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithCustomMatchable.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalledWithCustomMatchable { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Opera')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenCalledWith(new OperaPastryMatchable()); // Match not serializable types 22 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithCustomMatchable.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithJSONMatchable.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalledWithJSONMatchable { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenCalledWith(Argument.jsonEquals(new Pastry('Chocolatine'))); // Match not serializable types 22 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithJSONMatchable.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithTypeMatchable.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenCalledWithTypeMatchable { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenCalledWith(Argument.ofType(Pastry.class)); 22 | Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenCalledWithTypeMatchable.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenLastCalledWith.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasBeenLastCalledWith { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | myBakery.order(new Pastry('Chocolatine')); 18 | myBakery.order(new Pastry('Croissant')); 19 | 20 | // Assert 21 | Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Croissant')); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasBeenLastCalledWith.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasNotBeenCalled.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class HasNotBeenCalled { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | 15 | // Act 16 | 17 | // Assert 18 | Expect.that(planDeliverySpy).hasNotBeenCalled(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /force-app/recipes/classes/asserting/HasNotBeenCalled.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Behave.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class Behave { 9 | class MockImpl implements MethodSpy.SpyBehavior { 10 | public Object execute(final List params) { 11 | return Date.today().addDays(10); 12 | } 13 | } 14 | 15 | @IsTest 16 | static void recipe() { 17 | // Arrange 18 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 19 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 20 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 21 | planDeliverySpy.behaves(new MockImpl()); 22 | 23 | // Act 24 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 25 | 26 | // Assert 27 | Assert.areEqual(Date.today().addDays(10), order.deliveryDate); 28 | 29 | // Act 30 | order = myBakery.order(new Pastry('Opera')); 31 | 32 | // Assert 33 | Assert.areEqual(Date.today().addDays(10), order.deliveryDate); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Behave.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/BehaveOnce.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class BehaveOnce { 9 | class MockImpl implements MethodSpy.SpyBehavior { 10 | public Object execute(final List params) { 11 | return Date.today().addDays(10); 12 | } 13 | } 14 | 15 | @IsTest 16 | static void recipe() { 17 | // Arrange 18 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 19 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 20 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 21 | planDeliverySpy.behavesOnce(new MockImpl()); 22 | 23 | // Act 24 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 25 | 26 | // Assert 27 | Assert.areEqual(Date.today().addDays(10), order.deliveryDate); 28 | 29 | // Act 30 | order = myBakery.order(new Pastry('Opera')); 31 | 32 | // Assert 33 | Assert.isNull(order.deliveryDate); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/BehaveOnce.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/NoConfiguration.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class NoConfiguration { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | 16 | // Act 17 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 18 | 19 | // Assert 20 | Assert.areEqual(null, order.deliveryDate); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/NoConfiguration.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Returns.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class Returns { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.returns(Date.today().addDays(3)); 16 | 17 | // Act 18 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 19 | 20 | // Assert 21 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Returns.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ReturnsOnce.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ReturnsOnce { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.throwsException(new RecipeException()); 16 | planDeliverySpy.returnsOnce(Date.today().addDays(3)); 17 | 18 | // Act 19 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 20 | 21 | // Assert 22 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 23 | 24 | // Act 25 | try { 26 | myBakery.order(new Pastry('Opera')); 27 | 28 | // Assert 29 | Assert.fail('Expected exception was not thrown'); 30 | } catch (Exception ex) { 31 | Assert.isInstanceOfType(ex, RecipeException.class); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ReturnsOnce.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ReturnsThenThrows.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ReturnsThenThrows { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.returns(Date.today().addDays(3)); 16 | planDeliverySpy.throwsException(new RecipeException()); 17 | 18 | // Act 19 | try { 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.fail('Expected exception was not thrown'); 24 | } catch (Exception ex) { 25 | Assert.isInstanceOfType(ex, RecipeException.class); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ReturnsThenThrows.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Throws.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class Throws { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.throwsException(new RecipeException()); 16 | 17 | // Act 18 | try { 19 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 20 | 21 | // Assert 22 | Assert.fail('Expected exception was not thrown'); 23 | } catch (Exception ex) { 24 | Assert.isInstanceOfType(ex, RecipeException.class); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/Throws.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ThrowsOnce.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ThrowsOnce { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.returns(Date.today().addDays(3)); 16 | planDeliverySpy.throwsExceptionOnce(new RecipeException()); 17 | 18 | // Act 19 | try { 20 | myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.fail('Expected exception was not thrown'); 24 | } catch (Exception ex) { 25 | Assert.isInstanceOfType(ex, RecipeException.class); 26 | } 27 | 28 | // Act 29 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 30 | 31 | // Assert 32 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ThrowsOnce.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ThrowsThenReturns.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ThrowsThenReturns { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.throwsException(new RecipeException()); 16 | planDeliverySpy.returns(Date.today().addDays(3)); 17 | 18 | // Act 19 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 20 | 21 | // Assert 22 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/ThrowsThenReturns.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithCustomMatchable_ThenReturn.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithCustomMatchable_ThenReturn { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new OperaPastryMatchable()).thenReturn(Date.today().addDays(3)); 16 | 17 | // Act 18 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 19 | 20 | // Assert 21 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithCustomMatchable_ThenReturn.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithEqualMatching_ThenReturn.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithEqualMatching_ThenReturn { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 16 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenReturn(Date.today().addDays(2)); 17 | planDeliverySpy.whenCalledWith(Argument.ofType(Pastry.class)).thenReturn(Date.today().addDays(4)); // Order matter 18 | 19 | // Act 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.areEqual(Date.today().addDays(2), order.deliveryDate); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithEqualMatching_ThenReturn.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithJSONMatching_ThenReturn.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithJSONMatching_ThenReturn { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(Argument.jsonEquals(new Pastry('Opera'))).thenReturn(Date.today().addDays(4)); // Order matter 16 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 17 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenReturn(Date.today().addDays(2)); 18 | 19 | // Act 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.areEqual(Date.today().addDays(4), order.deliveryDate); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithJSONMatching_ThenReturn.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithMatchingThrowsAndReturns.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithMatchingThrowsAndReturns { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.returns(Date.today().addDays(2)); 16 | planDeliverySpy.throwsException(new RecipeException()); 17 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 18 | planDeliverySpy.whenCalledWith(new Pastry('Pain au Chocolat')).thenThrow(new RecipeException()); 19 | 20 | // Act 21 | try { 22 | OrderConfirmation order = myBakery.order(new Pastry('Pain au Chocolat')); 23 | 24 | // Assert 25 | Assert.fail('Expected exception was not thrown'); 26 | } catch (Exception ex) { 27 | Assert.isInstanceOfType(ex, RecipeException.class); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithMatchingThrowsAndReturns.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithNotMatchingAndReturn.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithNotMatchingAndReturn { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.returns(Date.today().addDays(4)); 16 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 17 | planDeliverySpy.whenCalledWith(new Pastry('Chocolatine')).thenThrow(new RecipeException()); 18 | 19 | // Act 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.areEqual(Date.today().addDays(4), order.deliveryDate); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithNotMatchingAndReturn.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithTypeMatching_ThenReturn.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithTypeMatching_ThenReturn { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(Argument.ofType(Pastry.class)).thenReturn(Date.today().addDays(4)); // Order matter 16 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 17 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenReturn(Date.today().addDays(2)); 18 | 19 | // Act 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.areEqual(Date.today().addDays(4), order.deliveryDate); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithTypeMatching_ThenReturn.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenBehave.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWith_ThenBehave { 9 | class MockImpl implements MethodSpy.SpyBehavior { 10 | public Object execute(final List params) { 11 | return Date.today().addDays(10); 12 | } 13 | } 14 | 15 | @IsTest 16 | static void recipe() { 17 | // Arrange 18 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 19 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 20 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 21 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenBehave(new MockImpl()); 22 | 23 | // Act 24 | OrderConfirmation order = myBakery.order(new Pastry('Croissant')); 25 | 26 | // Assert 27 | Assert.areEqual(Date.today().addDays(10), order.deliveryDate); 28 | 29 | // Act 30 | order = myBakery.order(new Pastry('Croissant')); 31 | 32 | // Assert 33 | Assert.areEqual(Date.today().addDays(10), order.deliveryDate); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenBehave.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenReturnOnce.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWith_ThenReturnOnce { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenThrow(new RecipeException()); 16 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturnOnce(Date.today().addDays(3)); 17 | 18 | // Act 19 | OrderConfirmation order = myBakery.order(new Pastry('Croissant')); 20 | 21 | // Assert 22 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 23 | 24 | // Act 25 | try { 26 | myBakery.order(new Pastry('Croissant')); 27 | 28 | // Assert 29 | Assert.fail('Expected exception was not thrown'); 30 | } catch (Exception ex) { 31 | Assert.isInstanceOfType(ex, RecipeException.class); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenReturnOnce.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenThrow.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWith_ThenThrow { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 16 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenThrow(new RecipeException()); 17 | 18 | // Act 19 | try { 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.fail('Expected exception was not thrown'); 24 | } catch (Exception ex) { 25 | Assert.isInstanceOfType(ex, RecipeException.class); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenThrow.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenThrowOnce.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWith_ThenThrowOnce { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenReturn(Date.today().addDays(3)); 16 | planDeliverySpy.whenCalledWith(new Pastry('Opera')).thenThrowOnce(new RecipeException()); 17 | 18 | // Act 19 | try { 20 | myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.fail('Expected exception was not thrown'); 24 | } catch (Exception ex) { 25 | Assert.isInstanceOfType(ex, RecipeException.class); 26 | } 27 | 28 | // Act 29 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 30 | 31 | // Assert 32 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWith_ThenThrowOnce.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithoutMatchingConfiguration.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class WhenCalledWithoutMatchingConfiguration { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | Mock deliveryServiceMock = Mock.forType(DeliveryService.class); 13 | MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Croissant')).thenReturn(Date.today().addDays(3)); 16 | planDeliverySpy.whenCalledWith(new Pastry('Chocolatine')).thenReturn(Date.today().addDays(2)); 17 | 18 | // Act 19 | try { 20 | OrderConfirmation order = myBakery.order(new Pastry('Opera')); 21 | 22 | // Assert 23 | Assert.fail('Expected exception was not thrown'); 24 | } catch (Exception ex) { 25 | Assert.isInstanceOfType(ex, MethodSpy.ConfigurationException.class); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /force-app/recipes/classes/mocking/WhenCalledWithoutMatchingConfiguration.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/Bakery.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | public class Bakery { 8 | private DeliveryService deliveryService; 9 | 10 | public Bakery(DeliveryService deliveryService) { 11 | this.deliveryService = deliveryService; 12 | } 13 | 14 | public OrderConfirmation order(Pastry pastry) { 15 | OrderConfirmation order = new OrderConfirmation(); 16 | order.pastry = pastry; 17 | order.deliveryDate = this.deliveryService.planDelivery(pastry); 18 | return order; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/Bakery.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/DeliveryService.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | public interface DeliveryService { 8 | Date planDelivery(Pastry pastry); 9 | } 10 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/DeliveryService.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/OrderConfirmation.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | public class OrderConfirmation { 8 | public Pastry pastry; 9 | public Date deliveryDate; 10 | } 11 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/OrderConfirmation.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/Pastry.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @JsonAccess(serializable='always') 8 | public class Pastry { 9 | public String name; 10 | 11 | public Pastry(String name) { 12 | this.name = name; 13 | } 14 | 15 | public Boolean equals(Object other) { 16 | if (other instanceof Pastry) { 17 | Pastry p = (Pastry) other; 18 | return (p.name == this.name); 19 | } 20 | return false; 21 | } 22 | 23 | public Integer hashCode() { 24 | return this.name?.hashCode(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/Pastry.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/RecipeException.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | public class RecipeException extends Exception { 8 | } 9 | -------------------------------------------------------------------------------- /force-app/recipes/classes/setup/RecipeException.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/src/classes/Argument.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /** 9 | * Matchable offers public static methods to build out-of-the-box matchers to be used with Expect 10 | * - Argument.any() 11 | * - Argument.equals(Object object) 12 | * - Argument.jsonEquals(Object object) 13 | * - Argument.ofType(String typeName) 14 | * - Argument.ofType(Type type) 15 | * - Argument.ofType(Schema.SObjectType sobjectType) 16 | */ 17 | @IsTest 18 | @SuppressWarnings('PMD.EmptyStatementBlock, PMD.AvoidGlobalModifier') 19 | global class Argument { 20 | global interface Matchable { 21 | Boolean matches(Object callArgument); 22 | } 23 | 24 | global class ConfigurationException extends Exception { 25 | } 26 | 27 | private Argument() { 28 | } 29 | 30 | public static boolean matches(final List matchableArguments, final List callArguments) { 31 | if (matchableArguments == null || callArguments == null) { 32 | // If both equals null then return true 33 | return matchableArguments == callArguments; 34 | } 35 | 36 | if (matchableArguments.size() != callArguments.size()) { 37 | return false; 38 | } 39 | 40 | for (Integer i = 0; i < matchableArguments.size(); ++i) { 41 | if (!matchableArguments[i].matches(callArguments[i])) { 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | } 48 | 49 | global static List empty() { 50 | return new List(); 51 | } 52 | 53 | global static List of(final Object arg) { 54 | return Argument.ofList(new List{ arg }); 55 | } 56 | 57 | global static List of(final Object arg1, final Object arg2) { 58 | return Argument.ofList(new List{ arg1, arg2 }); 59 | } 60 | 61 | global static List of(final Object arg1, final Object arg2, final Object arg3) { 62 | return Argument.ofList(new List{ arg1, arg2, arg3 }); 63 | } 64 | 65 | global static List of(final Object arg1, final Object arg2, final Object arg3, final Object arg4) { 66 | return Argument.ofList(new List{ arg1, arg2, arg3, arg4 }); 67 | } 68 | 69 | global static List of(final Object arg1, final Object arg2, final Object arg3, final Object arg4, final Object arg5) { 70 | return Argument.ofList(new List{ arg1, arg2, arg3, arg4, arg5 }); 71 | } 72 | 73 | global static List ofList(final List listOfArgs) { 74 | final List listOfMatchableArgs = Argument.empty(); 75 | if (listOfArgs == null) { 76 | return listOfMatchableArgs; 77 | } 78 | for (Object callArgument : listOfArgs) { 79 | if (callArgument instanceof Argument.Matchable) { 80 | listOfMatchableArgs.add((Argument.Matchable) callArgument); 81 | } else { 82 | listOfMatchableArgs.add(Argument.equals(callArgument)); 83 | } 84 | } 85 | return listOfMatchableArgs; 86 | } 87 | 88 | global static Argument.Matchable any() { 89 | return new AnyMatchable(); 90 | } 91 | 92 | global static Argument.Matchable equals(final Object callArgument) { 93 | return new EqualsMatchable(callArgument); 94 | } 95 | 96 | global static Argument.Matchable jsonEquals(final Object callArgument) { 97 | return new JSONMatchable(callArgument); 98 | } 99 | 100 | global static Argument.Matchable ofType(final String matchingType) { 101 | return new TypeMatchable(matchingType); 102 | } 103 | 104 | global static Argument.Matchable ofType(final Schema.SObjectType callArgument) { 105 | return new TypeMatchable(callArgument); 106 | } 107 | 108 | global static Argument.Matchable ofType(final Type callArgument) { 109 | return new TypeMatchable(callArgument); 110 | } 111 | 112 | private class AnyMatchable implements Argument.Matchable { 113 | public Boolean matches(final Object callArgument) { 114 | return true; 115 | } 116 | 117 | public override String toString() { 118 | return 'any'; 119 | } 120 | 121 | public Boolean equals(Object obj) { 122 | return obj != null && obj instanceof AnyMatchable; 123 | } 124 | } 125 | 126 | private class EqualsMatchable implements Argument.Matchable { 127 | private Object callArgumentToMatch; 128 | 129 | public EqualsMatchable(final Object callArgumentToMatch) { 130 | this.callArgumentToMatch = callArgumentToMatch; 131 | } 132 | 133 | public Boolean matches(final Object callArgument) { 134 | if (this.callArgumentToMatch == null) { 135 | return callArgument == null; 136 | } 137 | 138 | // case-sensitive check for string 139 | if (this.callArgumentToMatch instanceof String) { 140 | return this.callArgumentToMatch.equals(callArgument); 141 | } 142 | 143 | // native platform equality check for anything else 144 | return this.callArgumentToMatch == callArgument; 145 | } 146 | 147 | public override String toString() { 148 | return String.valueOf(this.callArgumentToMatch); 149 | } 150 | 151 | public Boolean equals(Object obj) { 152 | if (obj == null || !(obj instanceof EqualsMatchable)) { 153 | return false; 154 | } 155 | EqualsMatchable other = (EqualsMatchable) obj; 156 | return this.matches(other.callArgumentToMatch); 157 | } 158 | } 159 | 160 | private class JSONMatchable implements Argument.Matchable { 161 | private String jsonValue; 162 | 163 | public JSONMatchable(final Object callArgumentToMatch) { 164 | this.jsonValue = JSON.serialize(callArgumentToMatch); 165 | } 166 | 167 | public boolean matches(final Object callArgument) { 168 | return String.valueOf(this.jsonValue).equals(String.valueOf(JSON.serialize(callArgument))); 169 | } 170 | 171 | override public String toString() { 172 | return 'json(' + this.jsonValue + ')'; 173 | } 174 | 175 | public Boolean equals(Object obj) { 176 | if (obj == null || !(obj instanceof JSONMatchable)) { 177 | return false; 178 | } 179 | JSONMatchable other = (JSONMatchable) obj; 180 | return this.jsonValue.equals(other.jsonValue); 181 | } 182 | } 183 | 184 | private class TypeMatchable implements Argument.Matchable { 185 | private String typeNameToMatch; 186 | 187 | public TypeMatchable(final Schema.SObjectType callArgumentToMatch) { 188 | this.typeNameToMatch = callArgumentToMatch.getDescribe().getName(); 189 | } 190 | 191 | public TypeMatchable(final String callArgumentToMatch) { 192 | this.typeNameToMatch = callArgumentToMatch; 193 | } 194 | 195 | public TypeMatchable(final Type callArgumentToMatch) { 196 | this.typeNameToMatch = callArgumentToMatch.getName(); 197 | } 198 | 199 | public boolean matches(final Object callArgument) { 200 | String typeName = Argument.getTypeName(callArgument); 201 | if (this.typeNameToMatch == typeName) { 202 | return true; 203 | } 204 | 205 | Type actualType = Type.forName(typeName); 206 | Type expectedType = Type.forName(this.typeNameToMatch); 207 | if (expectedType != null && actualType != null) { 208 | return expectedType.isAssignableFrom(actualType); 209 | } 210 | return false; 211 | } 212 | 213 | override public String toString() { 214 | return this.typeNameToMatch + '.Type'; 215 | } 216 | 217 | public Boolean equals(Object obj) { 218 | if (obj == null || !(obj instanceof TypeMatchable)) { 219 | return false; 220 | } 221 | TypeMatchable other = (TypeMatchable) obj; 222 | return this.typeNameToMatch == other.typeNameToMatch; 223 | } 224 | } 225 | 226 | private static String getTypeName(final Object callArgument) { 227 | String result = 'Date'; 228 | try { 229 | Date typeCheck = (Date) callArgument; 230 | } catch (System.TypeException te) { 231 | String message = te.getMessage().substringAfter('Invalid conversion from runtime type '); 232 | result = message.substringBefore(' to Date'); 233 | } 234 | return result; 235 | } 236 | 237 | public static Type getType(final Object callArgument) { 238 | final String typeName = getTypeName(callArgument); 239 | return Type.forName(typeName); 240 | } 241 | 242 | global static Boolean areListsEqual(final List list1, final List list2) { 243 | if (list1 == null || list2 == null) { 244 | return list1 == list2; 245 | } 246 | 247 | final Integer size = list1.size(); 248 | if (size != list2.size()) { 249 | return false; 250 | } 251 | 252 | for (Integer i = 0; i < size; ++i) { 253 | if (list1[i] != list2[i]) { 254 | return false; 255 | } 256 | } 257 | return true; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /force-app/src/classes/Argument.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/src/classes/Expect.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | global class Expect { 9 | global interface MethodSpyExpectable { 10 | /** 11 | * Assert that a method spy has never been called 12 | */ 13 | void hasNotBeenCalled(); 14 | 15 | /** 16 | * Assert that a method spy has been called at least one time with any parameters 17 | */ 18 | void hasBeenCalled(); 19 | 20 | /** 21 | * Assert that a method spy has been called exactly N times with any parameters 22 | * 23 | * @param count number of times the method spy should have been called 24 | */ 25 | void hasBeenCalledTimes(final Integer count); 26 | 27 | /** 28 | * Assert that a method spy has been called at least one time with matching parameters 29 | * 30 | * @param param1 args that should match one call parameters 31 | * @see Arguments 32 | */ 33 | void hasBeenCalledWith(); 34 | void hasBeenCalledWith(final Object param1); 35 | void hasBeenCalledWith(final Object param1, final Object param2); 36 | void hasBeenCalledWith(final Object param1, final Object param2, final Object param3); 37 | void hasBeenCalledWith(final Object param1, final Object param2, final Object param3, final Object param4); 38 | void hasBeenCalledWith(final Object param1, final Object param2, final Object param3, final Object param4, final Object param5); 39 | 40 | /** 41 | * Assert that a method spy has been lastly called with matching parameters 42 | * 43 | * @param args args that should match last call parameters 44 | * @see Arguments 45 | */ 46 | void hasBeenLastCalledWith(); 47 | void hasBeenLastCalledWith(final Object param1); 48 | void hasBeenLastCalledWith(final Object param1, final Object param2); 49 | void hasBeenLastCalledWith(final Object param1, final Object param2, final Object param3); 50 | void hasBeenLastCalledWith(final Object param1, final Object param2, final Object param3, final Object param4); 51 | void hasBeenLastCalledWith(final Object param1, final Object param2, final Object param3, final Object param4, final Object param5); 52 | } 53 | 54 | global interface ErrorMessage { 55 | String toString(); 56 | } 57 | 58 | global interface Asserter { 59 | void isTrue(Boolean value, ErrorMessage message); 60 | void isFalse(Boolean value, ErrorMessage message); 61 | } 62 | 63 | /** 64 | * Starts to build assertions on a method spy 65 | * 66 | * @param spy the MethodSpy to assert 67 | * @see MethodSpy 68 | */ 69 | global static MethodSpyExpectable that(MethodSpy spy) { 70 | return that(spy, METHOD_SPY_ASSERTER); 71 | } 72 | 73 | /** 74 | * Starts to build assertions on a method spy using a custom asserter 75 | * 76 | * @param spy the MethodSpy to assert 77 | * @param asserter the Assert to use during assertions 78 | */ 79 | @TestVisible 80 | private static MethodSpyExpectable that(MethodSpy spy, Asserter asserter) { 81 | return new DefaultMethodSpyExpectable(spy, asserter); 82 | } 83 | 84 | @TestVisible 85 | private class MethodSpyAsserter implements Asserter { 86 | public void isTrue(Boolean value, ErrorMessage message) { 87 | // Do not use Assert.isTrue to check value 88 | // Because we want to compute ErrorMessage to string() 89 | // only when the Boolean is false 90 | if (!value) { 91 | Assert.fail(message.toString()); 92 | } 93 | } 94 | public void isFalse(Boolean value, ErrorMessage message) { 95 | // Do not use Assert.isFalse to check value 96 | // Because we want to compute ErrorMessage to string() 97 | // only when the Boolean is true 98 | if (value) { 99 | Assert.fail(message.toString()); 100 | } 101 | } 102 | } 103 | 104 | private static final Asserter METHOD_SPY_ASSERTER = new MethodSpyAsserter(); 105 | 106 | private class DefaultMethodSpyExpectable implements MethodSpyExpectable { 107 | private MethodSpy spy; 108 | private Asserter asserter; 109 | 110 | private DefaultMethodSpyExpectable(MethodSpy spy) { 111 | this.spy = spy; 112 | this.asserter = METHOD_SPY_ASSERTER; 113 | } 114 | 115 | private DefaultMethodSpyExpectable(MethodSpy spy, Asserter asserter) { 116 | this(spy); 117 | this.asserter = asserter; 118 | } 119 | 120 | public void hasNotBeenCalled() { 121 | this.asserter.isTrue(this.spy.callLog.isEmpty(), buildErrorMessage(this.spy, ErrorMessageType.CALLED)); 122 | } 123 | 124 | public void hasBeenCalled() { 125 | this.asserter.isFalse(this.spy.callLog.isEmpty(), buildErrorMessage(this.spy, ErrorMessageType.NEVER_CALLED)); 126 | } 127 | 128 | public void hasBeenCalledTimes(final Integer count) { 129 | this.asserter.isTrue(this.spy.callLog.size() == count, buildErrorMessage(this.spy, ErrorMessageType.NOT_CALLED_TIMES, count)); 130 | } 131 | 132 | public void hasBeenCalledWith() { 133 | this.hasBeenCalledWithArguments(Argument.empty()); 134 | } 135 | 136 | public void hasBeenCalledWith(final Object arg) { 137 | this.hasBeenCalledWithArguments((arg instanceof List) ? (List) arg : Argument.of(arg)); 138 | } 139 | 140 | public void hasBeenCalledWith(final Object arg1, final Object arg2) { 141 | this.hasBeenCalledWithArguments(Argument.of(arg1, arg2)); 142 | } 143 | 144 | public void hasBeenCalledWith(final Object arg1, final Object arg2, final Object arg3) { 145 | this.hasBeenCalledWithArguments(Argument.of(arg1, arg2, arg3)); 146 | } 147 | 148 | public void hasBeenCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4) { 149 | this.hasBeenCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4)); 150 | } 151 | 152 | public void hasBeenCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4, final Object arg5) { 153 | this.hasBeenCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4, arg5)); 154 | } 155 | 156 | private void hasBeenCalledWithArguments(final List args) { 157 | Boolean hasBeenCalledWithArguments = false; 158 | for (Integer i = 0; i < this.spy.callLog.size(); ++i) { 159 | if (Argument.matches(args, this.spy.callLog.get(i))) { 160 | hasBeenCalledWithArguments = true; 161 | break; 162 | } 163 | } 164 | this.asserter.isFalse(this.spy.callLog.isEmpty(), buildErrorMessage(this.spy, ErrorMessageType.NEVER_CALLED)); 165 | this.asserter.isTrue(hasBeenCalledWithArguments, buildErrorMessage(this.spy, ErrorMessageType.NOT_CALLED_WITH, args)); 166 | } 167 | 168 | public void hasBeenLastCalledWith() { 169 | this.hasBeenLastCalledWithArguments(Argument.empty()); 170 | } 171 | 172 | public void hasBeenLastCalledWith(final Object arg) { 173 | this.hasBeenLastCalledWithArguments((arg instanceof List) ? (List) arg : Argument.of(arg)); 174 | } 175 | 176 | public void hasBeenLastCalledWith(final Object arg1, final Object arg2) { 177 | this.hasBeenLastCalledWithArguments(Argument.of(arg1, arg2)); 178 | } 179 | 180 | public void hasBeenLastCalledWith(final Object arg1, final Object arg2, final Object arg3) { 181 | this.hasBeenLastCalledWithArguments(Argument.of(arg1, arg2, arg3)); 182 | } 183 | 184 | public void hasBeenLastCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4) { 185 | this.hasBeenLastCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4)); 186 | } 187 | 188 | public void hasBeenLastCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4, final Object arg5) { 189 | this.hasBeenLastCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4, arg5)); 190 | } 191 | 192 | private void hasBeenLastCalledWithArguments(final List args) { 193 | this.asserter.isFalse(this.spy.callLog.isEmpty(), buildErrorMessage(this.spy, ErrorMessageType.NEVER_CALLED)); 194 | this.asserter.isTrue(Argument.matches(args, this.spy.callLog.getLast()), buildErrorMessage(this.spy, ErrorMessageType.NOT_LAST_CALLED_WITH, args)); 195 | } 196 | } 197 | 198 | private enum ErrorMessageType { 199 | CALLED, 200 | NEVER_CALLED, 201 | NOT_CALLED_WITH, 202 | NOT_LAST_CALLED_WITH, 203 | NOT_CALLED_TIMES 204 | } 205 | 206 | private class ErrorMessageRow { 207 | protected String message; 208 | protected Boolean tabbed; 209 | 210 | ErrorMessageRow(String message) { 211 | this(message, false); 212 | } 213 | ErrorMessageRow(String message, Boolean tabbed) { 214 | this.message = message; 215 | this.tabbed = tabbed; 216 | } 217 | } 218 | 219 | private class ErrorMessageImpl implements ErrorMessage { 220 | private MethodSpy spy; 221 | private ErrorMessageType error; 222 | private Object argument; 223 | 224 | ErrorMessageImpl(MethodSpy spy, ErrorMessageType error) { 225 | this(spy, error, null); 226 | } 227 | ErrorMessageImpl(MethodSpy spy, ErrorMessageType error, Object argument) { 228 | this.spy = spy; 229 | this.error = error; 230 | this.argument = argument; 231 | } 232 | 233 | private String buildHeadline(MethodSpy spy, ErrorMessageType error, Object argument) { 234 | final Map messages = new Map{ 235 | ErrorMessageType.CALLED => 'Method {0} was called', 236 | ErrorMessageType.NEVER_CALLED => 'Method {0} was not called', 237 | ErrorMessageType.NOT_CALLED_WITH => 'Method {0} was not called with {1}', 238 | ErrorMessageType.NOT_LAST_CALLED_WITH => 'Method {0} was not last called with {1}', 239 | ErrorMessageType.NOT_CALLED_TIMES => 'Method {0} was not called {1} times' 240 | }; 241 | return String.format(messages.get(error), new List{ spy.methodName, argument + '' }); 242 | } 243 | 244 | private List buildCallTraces(MethodSpy spy) { 245 | final List> reversedCallLogArguments = new List>(); 246 | for (Integer i = this.spy.callLog.size() - 1; i >= 0; i--) { 247 | reversedCallLogArguments.add(this.spy.callLog.get(i)); 248 | } 249 | 250 | if (reversedCallLogArguments.isEmpty()) { 251 | return new List(); 252 | } 253 | 254 | final List rows = new List(); 255 | rows.add(new ErrorMessageRow('method call history:')); 256 | final String template = '#{0} {1}({2})'; 257 | for (Integer i = 0; i < reversedCallLogArguments.size(); i++) { 258 | final List args = reversedCallLogArguments[i]; 259 | final Integer count = reversedCallLogArguments.size() - i; 260 | rows.add(new ErrorMessageRow(String.format(template, new List{ count, spy.methodName, args + '' }), true)); 261 | } 262 | return rows; 263 | } 264 | 265 | override public String toString() { 266 | final String headline = this.buildHeadline(spy, error, argument); 267 | final List callTraces = this.buildCallTraces(spy); 268 | 269 | final List lines = new List(); 270 | lines.add(headline); 271 | if (!callTraces.isEmpty()) { 272 | for (ErrorMessageRow row : callTraces) { 273 | final String prefix = row.tabbed ? '\t' : ''; 274 | final String line = prefix + row.message; 275 | lines.add(line); 276 | } 277 | lines.add(''); 278 | } 279 | return String.join(lines, '\n'); 280 | } 281 | } 282 | 283 | private static ErrorMessage buildErrorMessage(MethodSpy spy, ErrorMessageType error) { 284 | return new ErrorMessageImpl(spy, error); 285 | } 286 | 287 | private static ErrorMessage buildErrorMessage(MethodSpy spy, ErrorMessageType error, Object argument) { 288 | return new ErrorMessageImpl(spy, error, argument); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /force-app/src/classes/Expect.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/src/classes/MethodSpy.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /** 9 | * MethodSpy represents an apex method spy on which you can register some value to be returned 10 | * - spy.returns(value): always return the same value using spy.returns(value) 11 | * - spy.whenCalledWith(Arguments args).thenReturn(value) 12 | * - spy.whenCalledWith(Arguments args).thenThrow(error) 13 | * note: the matching algorithm is based on Matchable specified in the Arguments instanciation (defaulting to equals) 14 | * @see Arguments 15 | * @see Matchable 16 | */ 17 | @IsTest 18 | global class MethodSpy { 19 | global final String methodName { get; private set; } 20 | global final CallLog callLog { get; private set; } 21 | private final List matchingBehaviorManagers; 22 | private final BehaviorManagement defaultBehaviorManager; 23 | 24 | global MethodSpy(String methodName) { 25 | this.methodName = methodName; 26 | this.callLog = new CallLog(); 27 | this.matchingBehaviorManagers = new List(); 28 | this.defaultBehaviorManager = new BehaviorManagement(); 29 | } 30 | 31 | // @deprecated use call(List paramTypes, List paramNames, List args) instead 32 | public Object call(List args) { 33 | // Forward compatibility implementation 34 | List paramTypes = new List(); 35 | List paramNames = new List(); 36 | 37 | for (Object arg : args) { 38 | paramTypes.add(Argument.getType(arg)); // Infer type dynamically 39 | paramNames.add(''); // Impossible to get this information at runtime 40 | } 41 | 42 | // Warn consumer 43 | System.Debug( 44 | LoggingLevel.WARN, 45 | '[apex-mockery] call to the deprecated MethodSpy.call(List args) method.\nPlease use MethodSpy.call(List paramTypes, List paramNames, List args) instead.' 46 | ); 47 | 48 | // Interoperability 49 | return this.call(paramTypes, paramNames, args); 50 | } 51 | 52 | public Object call(List paramTypes, List paramNames, List args) { 53 | this.callLog.add(new MethodCall(args)); 54 | 55 | if (!this.isConfigured()) { 56 | return null; 57 | } 58 | 59 | for (MatchingParamsMethodSpyConfig matchingConfig : this.matchingBehaviorManagers) { 60 | if (matchingConfig.matchesOnce(args)) { 61 | return matchingConfig.pollSpyBehavior(args); 62 | } 63 | } 64 | 65 | if (this.defaultBehaviorManager.hasConfiguredOnceBehavior()) { 66 | return this.defaultBehaviorManager.pollSpyBehavior(args); 67 | } 68 | 69 | for (MatchingParamsMethodSpyConfig matchingConfig : this.matchingBehaviorManagers) { 70 | if (matchingConfig.matches(args)) { 71 | return matchingConfig.pollSpyBehavior(args); 72 | } 73 | } 74 | 75 | if (this.defaultBehaviorManager.hasConfiguredGeneralBehavior()) { 76 | return this.defaultBehaviorManager.pollSpyBehavior(args); 77 | } 78 | 79 | throw new ConfigurationExceptionBuilder().withMethodSpy(this).withCallTypes(paramTypes).withCallParamNames(paramNames).withCallArguments(args).build(); 80 | } 81 | 82 | private Boolean isConfigured() { 83 | for (MatchingParamsMethodSpyConfig matchingConfig : this.matchingBehaviorManagers) { 84 | if (matchingConfig.hasConfiguration()) { 85 | return true; 86 | } 87 | } 88 | 89 | return this.defaultBehaviorManager.hasConfiguredOnceBehavior() || this.defaultBehaviorManager.hasConfiguredGeneralBehavior(); 90 | } 91 | 92 | global void behaves(final SpyBehavior impl) { 93 | this.defaultBehaviorManager.configure(impl); 94 | } 95 | 96 | global void behavesOnce(final SpyBehavior impl) { 97 | this.behaves(impl, 1); 98 | } 99 | 100 | global void behaves(final SpyBehavior impl, final Integer times) { 101 | this.defaultBehaviorManager.configure(impl, times); 102 | } 103 | 104 | global void returns(Object value) { 105 | this.defaultBehaviorManager.configure(new ConfiguredValueBehavior(value)); 106 | } 107 | 108 | global void throwsException(Exception exceptionToThrow) { 109 | this.defaultBehaviorManager.configure(new ConfiguredExceptionBehavior(exceptionToThrow)); 110 | } 111 | 112 | global void returnsOnce(Object value) { 113 | this.returns(value, 1); 114 | } 115 | 116 | global void throwsExceptionOnce(Exception exceptionToThrowOnce) { 117 | this.throwsException(exceptionToThrowOnce, 1); 118 | } 119 | 120 | global void returns(Object value, final Integer times) { 121 | this.defaultBehaviorManager.configure(new ConfiguredValueBehaviorOnce(value), times); 122 | } 123 | 124 | global void throwsException(Exception exceptionToThrowTimes, final Integer times) { 125 | this.defaultBehaviorManager.configure(new ConfiguredExceptionBehaviorOnce(exceptionToThrowTimes), times); 126 | } 127 | 128 | global MethodSpyCall whenCalledWith() { 129 | return this.whenCalledWithArguments(Argument.empty()); 130 | } 131 | 132 | global MethodSpyCall whenCalledWith(final Object arg) { 133 | return this.whenCalledWithArguments((arg instanceof List) ? (List) arg : Argument.of(arg)); 134 | } 135 | 136 | global MethodSpyCall whenCalledWith(final Object arg1, final Object arg2) { 137 | return this.whenCalledWithArguments(Argument.of(arg1, arg2)); 138 | } 139 | 140 | global MethodSpyCall whenCalledWith(final Object arg1, final Object arg2, final Object arg3) { 141 | return this.whenCalledWithArguments(Argument.of(arg1, arg2, arg3)); 142 | } 143 | 144 | global MethodSpyCall whenCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4) { 145 | return this.whenCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4)); 146 | } 147 | 148 | global MethodSpyCall whenCalledWith(final Object arg1, final Object arg2, final Object arg3, final Object arg4, final Object arg5) { 149 | return this.whenCalledWithArguments(Argument.of(arg1, arg2, arg3, arg4, arg5)); 150 | } 151 | 152 | private MethodSpyCall whenCalledWithArguments(final List args) { 153 | for (MatchingParamsMethodSpyConfig matchingConfig : this.matchingBehaviorManagers) { 154 | if (Argument.areListsEqual(args, matchingConfig.argsMatchable)) { 155 | return matchingConfig; 156 | } 157 | } 158 | 159 | final MatchingParamsMethodSpyConfig parameterizedMethodCall = new MatchingParamsMethodSpyConfig(args); 160 | this.matchingBehaviorManagers.add(parameterizedMethodCall); 161 | return parameterizedMethodCall; 162 | } 163 | 164 | public class CallLog { 165 | private List callArguments = new List(); 166 | 167 | private void add(MethodCall callParam) { 168 | this.callArguments.add(callParam); 169 | } 170 | 171 | public Boolean isEmpty() { 172 | return this.callArguments.isEmpty(); 173 | } 174 | 175 | public Integer size() { 176 | return this.callArguments.size(); 177 | } 178 | 179 | public List get(final Integer index) { 180 | return this.callArguments[index].args; 181 | } 182 | 183 | public List getLast() { 184 | return this.size() > 0 ? this.get(this.size() - 1) : null; 185 | } 186 | } 187 | 188 | private class MethodCall { 189 | public List args { get; private set; } 190 | 191 | public MethodCall(final List args) { 192 | this.args = args; 193 | } 194 | } 195 | 196 | global interface MethodSpyCall { 197 | MethodSpyCall thenReturn(Object value); 198 | MethodSpyCall thenThrow(Exception error); 199 | MethodSpyCall thenBehave(SpyBehavior impl); 200 | MethodSpyCall thenReturnOnce(Object value); 201 | MethodSpyCall thenThrowOnce(Exception error); 202 | MethodSpyCall thenBehaveOnce(SpyBehavior impl); 203 | MethodSpyCall thenReturn(Object value, Integer times); 204 | MethodSpyCall thenThrow(Exception error, Integer times); 205 | MethodSpyCall thenBehave(SpyBehavior impl, Integer times); 206 | } 207 | 208 | private class MatchingParamsMethodSpyConfig implements MethodSpyCall { 209 | private final List argsMatchable; 210 | private final BehaviorManagement behaviorManager; 211 | 212 | public MatchingParamsMethodSpyConfig(final List argsMatchable) { 213 | this.argsMatchable = argsMatchable; 214 | this.behaviorManager = new BehaviorManagement(); 215 | } 216 | 217 | public MethodSpyCall thenBehave(final SpyBehavior impl) { 218 | this.behaviorManager.configure(impl); 219 | return this; 220 | } 221 | 222 | public MethodSpyCall thenBehaveOnce(final SpyBehavior impl) { 223 | this.thenBehave(impl, 1); 224 | return this; 225 | } 226 | 227 | public MethodSpyCall thenBehave(final SpyBehavior impl, final Integer times) { 228 | this.behaviorManager.configure(impl, times); 229 | return this; 230 | } 231 | 232 | public MethodSpyCall thenReturn(final Object value) { 233 | this.behaviorManager.configure(new ConfiguredValueBehavior(value)); 234 | return this; 235 | } 236 | 237 | public MethodSpyCall thenThrow(final Exception exceptionToThrow) { 238 | this.behaviorManager.configure(new ConfiguredExceptionBehavior(exceptionToThrow)); 239 | return this; 240 | } 241 | 242 | public MethodSpyCall thenReturnOnce(final Object value) { 243 | return this.thenReturn(value, 1); 244 | } 245 | 246 | public MethodSpyCall thenThrowOnce(final Exception exceptionToThrowOnce) { 247 | return this.thenThrow(exceptionToThrowOnce, 1); 248 | } 249 | 250 | public MethodSpyCall thenReturn(final Object value, final Integer times) { 251 | this.behaviorManager.configure(new ConfiguredValueBehaviorOnce(value), times); 252 | return this; 253 | } 254 | 255 | public MethodSpyCall thenThrow(final Exception exceptionToThrowTimes, final Integer times) { 256 | this.behaviorManager.configure(new ConfiguredExceptionBehaviorOnce(exceptionToThrowTimes), times); 257 | return this; 258 | } 259 | 260 | public Boolean hasConfiguration() { 261 | return this.behaviorManager.hasConfiguredOnceBehavior() || this.behaviorManager.hasConfiguredGeneralBehavior(); 262 | } 263 | 264 | public Object pollSpyBehavior(final List args) { 265 | return this.behaviorManager.pollSpyBehavior(args); 266 | } 267 | 268 | public Boolean matches(final List callArguments) { 269 | return this.behaviorManager.hasConfiguredGeneralBehavior() && Argument.matches(this.argsMatchable, callArguments); 270 | } 271 | 272 | public Boolean matchesOnce(final List callArguments) { 273 | return this.behaviorManager.hasConfiguredOnceBehavior() && Argument.matches(this.argsMatchable, callArguments); 274 | } 275 | 276 | public override String toString() { 277 | return 'whenCalledWith' + this.argsMatchable + this.behaviorManager; 278 | } 279 | } 280 | 281 | private class ConfigurationExceptionBuilder { 282 | private MethodSpy spy; 283 | private List callTypes; 284 | private List callParamNames; 285 | private List callArguments; 286 | 287 | public ConfigurationExceptionBuilder withMethodSpy(final MethodSpy spy) { 288 | this.spy = spy; 289 | return this; 290 | } 291 | 292 | public ConfigurationExceptionBuilder withCallTypes(final List callTypes) { 293 | this.callTypes = callTypes; 294 | return this; 295 | } 296 | 297 | public ConfigurationExceptionBuilder withCallParamNames(final List callParamNames) { 298 | this.callParamNames = callParamNames; 299 | return this; 300 | } 301 | 302 | public ConfigurationExceptionBuilder withCallArguments(final List callArguments) { 303 | this.callArguments = callArguments; 304 | return this; 305 | } 306 | 307 | public ConfigurationException build() { 308 | List errorMessages = new List(); 309 | for (MethodSpyCall methodCall : this.spy.matchingBehaviorManagers) { 310 | errorMessages.add(methodCall.toString()); 311 | } 312 | 313 | List callArgumentsAsString = new List(); 314 | for (Integer i = 0; i < this.callArguments.size(); i++) { 315 | callArgumentsAsString.add(this.callTypes[i] + ' ' + this.callParamNames[i] + '[' + this.callArguments[i] + ']'); 316 | } 317 | return new ConfigurationException( 318 | 'No stub value found for a call of ' + 319 | this.spy.methodName + 320 | '(' + 321 | String.join(callArgumentsAsString, ', ') + 322 | ')' + 323 | '\nHere are the configured stubs:\n\t' + 324 | String.join(errorMessages, '\n\t') 325 | ); 326 | } 327 | } 328 | 329 | private class BehaviorManagement { 330 | private final List onceBehaviors = new List(); 331 | private SpyBehavior generalBehavior; 332 | 333 | public void configure(final SpyBehavior behavior) { 334 | this.configure(behavior, 0); 335 | } 336 | 337 | public void configure(final SpyBehavior behavior, final Integer times) { 338 | if (times > 0) { 339 | for (Integer i = 0; i < times; ++i) { 340 | this.onceBehaviors.add(behavior); 341 | } 342 | } else if (times == 0) { 343 | this.generalBehavior = behavior; 344 | } 345 | } 346 | 347 | public Boolean hasConfiguredOnceBehavior() { 348 | return this.onceBehaviors.size() > 0; 349 | } 350 | 351 | public Boolean hasConfiguredGeneralBehavior() { 352 | return this.generalBehavior != null; 353 | } 354 | 355 | public Object pollSpyBehavior(final List args) { 356 | final SpyBehavior behaviorToApply = this.hasConfiguredOnceBehavior() ? this.onceBehaviors.remove(0) : this.generalBehavior; 357 | return behaviorToApply?.execute(args); 358 | } 359 | 360 | public override String toString() { 361 | final List oncesString = new List(); 362 | for (SpyBehavior configuredBehaviorOnce : this.onceBehaviors) { 363 | oncesString.add(configuredBehaviorOnce.toString()); 364 | } 365 | return String.join(oncesString, '') + (this.generalBehavior != null ? this.generalBehavior.toString() : ''); 366 | } 367 | } 368 | 369 | global interface SpyBehavior { 370 | Object execute(List params); 371 | } 372 | 373 | private virtual class ConfiguredValueBehavior implements SpyBehavior { 374 | public final Object value; 375 | public ConfiguredValueBehavior(final Object value) { 376 | this.value = value; 377 | } 378 | 379 | public Object execute(final List params) { 380 | return this.value; 381 | } 382 | 383 | public override virtual String toString() { 384 | return '.thenReturn(' + this.value + ')'; 385 | } 386 | } 387 | 388 | private class ConfiguredValueBehaviorOnce extends ConfiguredValueBehavior { 389 | public ConfiguredValueBehaviorOnce(final Object value) { 390 | super(value); 391 | } 392 | public override String toString() { 393 | return '.thenReturnOnce(' + this.value + ')'; 394 | } 395 | } 396 | 397 | private virtual class ConfiguredExceptionBehavior implements SpyBehavior { 398 | public final Exception error; 399 | public ConfiguredExceptionBehavior(final Exception error) { 400 | this.error = error; 401 | } 402 | 403 | public Object execute(final List params) { 404 | throw (Exception) this.error; 405 | } 406 | 407 | public override virtual String toString() { 408 | return '.thenThrow(' + this.error + ')'; 409 | } 410 | } 411 | 412 | private class ConfiguredExceptionBehaviorOnce extends ConfiguredExceptionBehavior { 413 | public ConfiguredExceptionBehaviorOnce(final Exception error) { 414 | super(error); 415 | } 416 | public override String toString() { 417 | return '.thenThrowOnce(' + this.error + ')'; 418 | } 419 | } 420 | 421 | global class ConfigurationException extends Exception { 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /force-app/src/classes/MethodSpy.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/src/classes/Mock.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | global class Mock implements System.StubProvider { 9 | global Object stub { get; set; } 10 | private Map spies = new Map(); 11 | 12 | private Mock(final Type aType, final StubBuilder stubBuilder) { 13 | this.stub = stubBuilder.build(aType, this); 14 | } 15 | 16 | global Object handleMethodCall( 17 | Object stubbedObject, 18 | String stubbedMethodName, 19 | Type returnType, 20 | List listOfParamTypes, 21 | List listOfParamNames, 22 | List listOfArgs 23 | ) { 24 | Object result; 25 | final String key = this.getSpyKey(stubbedMethodName); 26 | if (this.spies.containsKey(key)) { 27 | MethodSpy spy = this.getSpy(key); 28 | result = spy.call(listOfParamTypes, listOfParamNames, listOfArgs); 29 | } 30 | 31 | return result; 32 | } 33 | 34 | global MethodSpy spyOn(final String methodName) { 35 | final String key = this.getSpyKey(methodName); 36 | if (!this.spies.containsKey(key)) { 37 | this.spies.put(key, new MethodSpy(methodName)); 38 | } 39 | return this.getSpy(key); 40 | } 41 | 42 | global MethodSpy getSpy(final String methodName) { 43 | return this.spies.get(this.getSpyKey(methodName)); 44 | } 45 | 46 | global static Mock forType(final Type aType) { 47 | return Mock.forType(aType, new DefaultStubBuilder()); 48 | } 49 | 50 | global static Mock forType(final Type aType, final StubBuilder stubBuilder) { 51 | return new Mock(aType, stubBuilder); 52 | } 53 | 54 | global interface StubBuilder { 55 | Object build(final Type aType, System.StubProvider stubProvider); 56 | } 57 | 58 | private String getSpyKey(final String methodName) { 59 | return methodName.toLowerCase(); 60 | } 61 | 62 | private class DefaultStubBuilder implements StubBuilder { 63 | public Object build(final Type aType, System.StubProvider stubProvider) { 64 | return Test.createStub(aType, stubProvider); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /force-app/src/classes/Mock.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/namespace/classes/ApexMockeryOverview.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ApexMockeryOverview { 9 | @IsTest 10 | static void recipe() { 11 | // Arrange 12 | mockery.Mock deliveryServiceMock = mockery.Mock.forType(DeliveryService.class, new StubBuilderImpl()); 13 | mockery.MethodSpy planDeliverySpy = deliveryServiceMock.spyOn('planDelivery'); 14 | Bakery myBakery = new Bakery((DeliveryService) deliveryServiceMock.stub); 15 | planDeliverySpy.whenCalledWith(new Pastry('Chocolatine')).thenReturn(Date.today().addDays(3)).thenReturnOnce(Date.today().addDays(2)); 16 | planDeliverySpy.whenCalledWith(new OperaPastryMatchable()).thenThrow(new RecipeException()); 17 | planDeliverySpy.returns(Date.today().addDays(4)); 18 | 19 | mockery.Expect.that(planDeliverySpy).hasNotBeenCalled(); 20 | mockery.Expect.that(planDeliverySpy).hasBeenCalledTimes(0); 21 | 22 | // Act & Assert 23 | // Once matcher (imagine first command is delivered early) 24 | OrderConfirmation order = myBakery.order(new Pastry('Chocolatine')); 25 | mockery.Expect.that(planDeliverySpy).hasBeenCalled(); 26 | mockery.Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 27 | mockery.Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Chocolatine')); 28 | mockery.Expect.that(planDeliverySpy).hasBeenCalledTimes(1); 29 | Assert.areEqual(Date.today().addDays(2), order.deliveryDate); 30 | 31 | // Matcher 32 | order = myBakery.order(new Pastry('Chocolatine')); 33 | mockery.Expect.that(planDeliverySpy).hasBeenCalled(); 34 | mockery.Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Chocolatine')); 35 | mockery.Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Chocolatine')); 36 | mockery.Expect.that(planDeliverySpy).hasBeenCalledTimes(2); 37 | Assert.areEqual(Date.today().addDays(3), order.deliveryDate); 38 | 39 | order = myBakery.order(new Pastry('Croissant')); 40 | mockery.Expect.that(planDeliverySpy).hasBeenCalled(); 41 | mockery.Expect.that(planDeliverySpy).hasBeenCalledWith(mockery.Argument.jsonEquals(new Pastry('Chocolatine'))); 42 | mockery.Expect.that(planDeliverySpy).hasBeenCalledWith(new Pastry('Croissant')); 43 | mockery.Expect.that(planDeliverySpy).hasBeenLastCalledWith(new Pastry('Croissant')); 44 | mockery.Expect.that(planDeliverySpy).hasBeenCalledTimes(3); 45 | Assert.areEqual(Date.today().addDays(4), order.deliveryDate); 46 | 47 | try { 48 | order = myBakery.order(new Pastry('Opera')); 49 | Assert.fail('Expected exception was not thrown'); 50 | } catch (Exception ex) { 51 | Assert.isInstanceOfType(ex, RecipeException.class); 52 | } 53 | } 54 | 55 | private class OperaPastryMatchable implements mockery.Argument.Matchable { 56 | public Boolean matches(Object callArgument) { 57 | Pastry p = (Pastry) callArgument; 58 | return p.name == 'Opera'; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /force-app/test/namespace/classes/ApexMockeryOverview.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/namespace/classes/utils/StubBuilderImpl.cls: -------------------------------------------------------------------------------- 1 | public class StubBuilderImpl implements mockery.Mock.StubBuilder { 2 | public Object build(final Type aType, System.StubProvider stubProvider) { 3 | return Test.createStub(aType, stubProvider); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /force-app/test/namespace/classes/utils/StubBuilderImpl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/BusinessService.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | public class BusinessService { 3 | DMLDelegate dmlDelegate; 4 | 5 | public BusinessService(DMLDelegate dmlDelegate) { 6 | this.dmlDelegate = dmlDelegate; 7 | } 8 | 9 | public void createNewCompany(Integer numberOfEmployees) { 10 | Account anAccount = new Account(NumberOfEmployees = numberOfEmployees); 11 | this.dmlDelegate.insertSObject(anAccount); 12 | } 13 | 14 | public void createNewCompanies(List numberOfEmployeesList) { 15 | System.Savepoint sp = Database.setSavepoint(); 16 | List accountToInsert = new List(); 17 | for (Integer numberOfEmployees : numberOfEmployeesList) { 18 | accountToInsert.add(new Account(NumberOfEmployees = numberOfEmployees)); 19 | } 20 | Database.DMLOptions dmlOptions = new Database.DMLOptions(); 21 | dmlOptions.optAllOrNone = false; 22 | Database.SaveResult[] saveResults = this.dmlDelegate.insertSObjects(accountToInsert, dmlOptions); 23 | Boolean allSucceed = true; 24 | for (Database.SaveResult saveResult : saveResults) { 25 | allSucceed &= saveResult.isSuccess(); 26 | } 27 | if (!allSucceed) { 28 | this.dmlDelegate.rollBack(sp); 29 | } 30 | } 31 | 32 | public void createNewCompanyWithOpportunity(Integer numberOfEmployees) { 33 | Account anAccount = new Account(NumberOfEmployees = numberOfEmployees); 34 | Database.SaveResult accountSaveResult = this.dmlDelegate.insertSObject(anAccount); 35 | Opportunity anOppy = new Opportunity(AccountId = accountSaveResult.getId()); 36 | this.dmlDelegate.insertSObject(anOppy); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/BusinessService.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/DMLDelegate.cls: -------------------------------------------------------------------------------- 1 | public interface DMLDelegate { 2 | // insert can throw DMLException when not allowing partial result (dmlOptions.optAllOrNone = true) 3 | Database.SaveResult insertSObject(SObject recordToInsert); 4 | Database.SaveResult insertSObject(SObject recordToInsert, Database.DMLOptions dmlOptions); 5 | Database.SaveResult[] insertSObjects(List recordsToInsert); 6 | Database.SaveResult[] insertSObjects(List recordsToInsert, Database.DMLOptions dmlOptions); 7 | void rollBack(System.Savepoint sp); 8 | } 9 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/DMLDelegate.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/FunctionalTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class FunctionalTest { 9 | @IsTest 10 | static void whenCreatingNewCompany_itShouldInsertAccount() { 11 | // Arrange 12 | Mock dmlDelegateMock = Mock.forType(DMLDelegate.class); 13 | MethodSpy insertSObjectSpy = dmlDelegateMock.spyOn('insertSObject'); 14 | BusinessService sut = new BusinessService((DMLDelegate) dmlDelegateMock.stub); 15 | 16 | // Act 17 | sut.createNewCompany(10); 18 | 19 | // Assert 20 | Expect.that(insertSObjectSpy).hasBeenCalledWith(Argument.of(new Account(NumberOfEmployees = 10))); 21 | } 22 | 23 | @IsTest 24 | static void whenCreatingNewCompanyWithOpportunity_itShouldInsertAccountAndOpportunity() { 25 | // Arrange 26 | Mock dmlDelegateMock = Mock.forType(DMLDelegate.class); 27 | MethodSpy insertSObjectSpy = dmlDelegateMock.spyOn('insertSObject'); 28 | 29 | insertSObjectSpy.whenCalledWith(Argument.of(Argument.ofType(Account.getSObjectType()))).thenReturn(succeed('001000000000000AAA')); 30 | 31 | insertSObjectSpy.whenCalledWith(Argument.of(Argument.ofType(Opportunity.getSObjectType()))).thenReturnOnce(succeed('006000000000000AAA')); 32 | 33 | BusinessService sut = new BusinessService((DMLDelegate) dmlDelegateMock.stub); 34 | 35 | // Act 36 | sut.createNewCompanyWithOpportunity(10); 37 | 38 | // Assert 39 | Account expectedAccount = new Account(NumberOfEmployees = 10); 40 | Expect.that(insertSObjectSpy).hasBeenCalledWith(Argument.of(expectedAccount)); 41 | Expect.that(insertSObjectSpy).hasBeenCalledWith(Argument.of(new Opportunity(AccountId = '001000000000000AAA'))); 42 | Expect.that(insertSObjectSpy).hasBeenCalledTimes(2); 43 | } 44 | 45 | @IsTest 46 | static void whenCreatingNewCompanyWith1BEmployees_itThrows() { 47 | // Arrange 48 | Mock dmlDelegateMock = Mock.forType(DMLDelegate.class); 49 | MethodSpy insertSObjectSpy = dmlDelegateMock.spyOn('insertSObject'); 50 | insertSObjectSpy.throwsException(new DmlException('1 billion person cannot work in the same company')); 51 | 52 | BusinessService sut = new BusinessService((DMLDelegate) dmlDelegateMock.stub); 53 | 54 | // Act 55 | try { 56 | sut.createNewCompany(1000000000); 57 | 58 | // Assert 59 | Assert.fail('it shoud not reach this line'); 60 | } catch (DmlException dex) { 61 | Expect.that(insertSObjectSpy).hasBeenCalledWith(Argument.of(new Account(NumberOfEmployees = 1000000000))); 62 | } 63 | } 64 | 65 | @IsTest 66 | static void whenCreatingNewCompaniesWithAllOrNoneFalseAndPartialSuccess_itReturnsSaveResult() { 67 | // Arrange 68 | Mock dmlDelegateMock = Mock.forType(DMLDelegate.class); 69 | MethodSpy insertSObjectsSpy = dmlDelegateMock.spyOn('insertSObjects'); 70 | MethodSpy rollbackSpy = dmlDelegateMock.spyOn('rollBack'); 71 | 72 | insertSObjectsSpy.returns( 73 | partial(new List{ '001000000000000AAA' }, new List{ new DBError('Error is located approximately between the chair and the monitor', '30') }) 74 | ); 75 | 76 | BusinessService sut = new BusinessService((DMLDelegate) dmlDelegateMock.stub); 77 | 78 | // Act 79 | sut.createNewCompanies(new List{ 10, 10 }); 80 | 81 | // Assert 82 | Database.DMLOptions dmlOptions = new Database.DMLOptions(); 83 | dmlOptions.optAllOrNone = false; 84 | Account expectedAccount = new Account(NumberOfEmployees = 10); 85 | Expect.that(insertSObjectsSpy) 86 | .hasBeenCalledWith(Argument.of(Argument.jsonEquals(new List{ expectedAccount, expectedAccount }), Argument.jsonEquals(dmlOptions))); 87 | Expect.that(rollbackSpy).hasBeenCalled(); 88 | } 89 | 90 | static Database.SaveResult succeed(Id recordId) { 91 | return (Database.SaveResult) JSON.deserialize('{"success":true,"id":"' + recordId + '"}', Database.SaveResult.class); 92 | } 93 | 94 | static Database.SaveResult[] succeed(List recordIds) { 95 | List results = new List(); 96 | for (Id recordId : recordIds) { 97 | results.add(succeed(recordId)); 98 | } 99 | return results; 100 | } 101 | 102 | static Database.SaveResult failed(DBError dbError) { 103 | return (Database.SaveResult) JSON.deserialize( 104 | '{"success":false,"errors":[{"message":"' + dbError.message + '","statusCode":"' + dbError.statusCode + '"}]}', 105 | Database.SaveResult.class 106 | ); 107 | } 108 | 109 | static Database.SaveResult[] failed(List dbErrors) { 110 | List results = new List(); 111 | for (DBError dbError : dbErrors) { 112 | results.add(failed(dbError)); 113 | } 114 | return results; 115 | } 116 | 117 | static Database.SaveResult[] partial(List successIds, List dbErrors) { 118 | List results = new List(); 119 | 120 | results.addall(succeed(successIds)); 121 | results.addall(failed(dbErrors)); 122 | 123 | return results; 124 | } 125 | 126 | class DBError { 127 | public String message; 128 | public String statusCode; 129 | 130 | public DBError(String message, String statusCode) { 131 | this.message = message; 132 | this.statusCode = statusCode; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /force-app/test/package/classes/functional/FunctionalTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/ArgumentTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/DummyInterface.cls: -------------------------------------------------------------------------------- 1 | public interface DummyInterface { 2 | String dummyMethod(); 3 | } 4 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/DummyInterface.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/ExpectTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class ExpectTest { 9 | // As Assert.* methods throws are not catchable 10 | // Test are white box (implementation test instead of behavioural test) 11 | // We implemented a FakeAsserter class for the occasion 12 | 13 | @IsTest 14 | static void givenMethodSpyInstance_thatReturnsMethodSpyAssertInstance() { 15 | // Arrange 16 | MethodSpy spy = new MethodSpy('method'); 17 | 18 | // Act 19 | Expect.MethodSpyExpectable result = Expect.that(spy); 20 | 21 | // Assert 22 | Assert.isNotNull(result); 23 | } 24 | 25 | @IsTest 26 | static void hasNotBeenCalled_callsAssertEquals() { 27 | // Arrange 28 | MethodSpy spy = new MethodSpy('method'); 29 | FakeAsserter fakeAsserter = new FakeAsserter(); 30 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 31 | 32 | // Act 33 | sut.hasNotBeenCalled(); 34 | 35 | // Assert 36 | Assert.areEqual(1, fakeAsserter.callCount); 37 | Assert.isFalse(fakeAsserter.failed); 38 | } 39 | 40 | @IsTest 41 | static void hasBeenCalled_callsAssertEquals() { 42 | // Arrange 43 | MethodSpy spy = new MethodSpy('method'); 44 | FakeAsserter fakeAsserter = new FakeAsserter(); 45 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 46 | 47 | // Act 48 | sut.hasBeenCalled(); 49 | 50 | // Assert 51 | Assert.areEqual(1, fakeAsserter.callCount); 52 | Assert.isTrue(fakeAsserter.failed); 53 | Assert.areEqual('Method method was not called', fakeAsserter.errorMessage); 54 | } 55 | 56 | @IsTest 57 | static void givenObject_hasBeenCalledWith_callsAssertEquals() { 58 | // Arrange 59 | MethodSpy spy = new MethodSpy('method'); 60 | FakeAsserter fakeAsserter = new FakeAsserter(); 61 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 62 | 63 | // Act 64 | sut.hasBeenCalledWith(new Account()); 65 | 66 | // Assert 67 | Assert.areEqual(2, fakeAsserter.callCount); 68 | Assert.isTrue(fakeAsserter.failed); 69 | Assert.areEqual('Method method was not called with (Account:{})', fakeAsserter.errorMessage); 70 | } 71 | 72 | @IsTest 73 | static void givenObjectList_hasBeenCalledWith_callsAssertEquals() { 74 | // Arrange 75 | MethodSpy spy = new MethodSpy('method'); 76 | FakeAsserter fakeAsserter = new FakeAsserter(); 77 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 78 | 79 | // Act 80 | sut.hasBeenCalledWith(Argument.ofList(new List{ new Account(), new Opportunity() })); 81 | 82 | // Assert 83 | Assert.areEqual(2, fakeAsserter.callCount); 84 | Assert.isTrue(fakeAsserter.failed); 85 | Assert.areEqual('Method method was not called with (Account:{}, Opportunity:{})', fakeAsserter.errorMessage); 86 | } 87 | 88 | @IsTest 89 | static void givenMixList_hasBeenCalledWith_callsAssertEquals() { 90 | // Arrange 91 | MethodSpy spy = new MethodSpy('method'); 92 | FakeAsserter fakeAsserter = new FakeAsserter(); 93 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 94 | 95 | // Act 96 | sut.hasBeenCalledWith(new Account(), Argument.equals('test')); 97 | 98 | // Assert 99 | Assert.areEqual(2, fakeAsserter.callCount); 100 | Assert.isTrue(fakeAsserter.failed); 101 | Assert.areEqual('Method method was not called with (Account:{}, test)', fakeAsserter.errorMessage); 102 | } 103 | 104 | @IsTest 105 | static void givenArgumentsEmpty_hasBeenCalledWith_callsAssertEquals() { 106 | // Arrange 107 | MethodSpy spy = new MethodSpy('method'); 108 | FakeAsserter fakeAsserter = new FakeAsserter(); 109 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 110 | 111 | // Act 112 | sut.hasBeenCalledWith(Argument.empty()); 113 | 114 | // Assert 115 | Assert.areEqual(2, fakeAsserter.callCount); 116 | Assert.isTrue(fakeAsserter.failed); 117 | Assert.areEqual('Method method was not called with ()', fakeAsserter.errorMessage); 118 | } 119 | 120 | @IsTest 121 | static void givenNoParameter_hasBeenCalledWith_callsAssertEquals() { 122 | // Arrange 123 | MethodSpy spy = new MethodSpy('method'); 124 | FakeAsserter fakeAsserter = new FakeAsserter(); 125 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 126 | 127 | // Act 128 | sut.hasBeenCalledWith(); 129 | 130 | // Assert 131 | Assert.areEqual(2, fakeAsserter.callCount); 132 | Assert.isTrue(fakeAsserter.failed); 133 | Assert.areEqual('Method method was not called with ()', fakeAsserter.errorMessage); 134 | } 135 | 136 | @IsTest 137 | static void given1Arguments_hasBeenCalledWith_callsAssertEquals() { 138 | // Arrange 139 | MethodSpy spy = new MethodSpy('method'); 140 | spy.returns('anything'); 141 | FakeAsserter fakeAsserter = new FakeAsserter(); 142 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 143 | spy.call(new List{ String.class }, new List{ 'name' }, new List{ 'param' }); 144 | 145 | // Act 146 | sut.hasBeenCalledWith(Argument.of('param')); 147 | 148 | // Assert 149 | Assert.areEqual(2, fakeAsserter.callCount); 150 | Assert.isFalse(fakeAsserter.failed); 151 | } 152 | 153 | @IsTest 154 | static void given2Arguments_hasBeenCalledWith_callsAssertEquals() { 155 | // Arrange 156 | MethodSpy spy = new MethodSpy('method'); 157 | spy.returns('anything'); 158 | FakeAsserter fakeAsserter = new FakeAsserter(); 159 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 160 | spy.call(new List{ Integer.class, String.class }, new List{ 'count', 'name' }, new List{ '2', 'param' }); 161 | 162 | // Act 163 | sut.hasBeenCalledWith('2', 'param'); 164 | 165 | // Assert 166 | Assert.areEqual(2, fakeAsserter.callCount); 167 | Assert.isFalse(fakeAsserter.failed); 168 | } 169 | 170 | @IsTest 171 | static void given3Arguments_hasBeenCalledWith_callsAssertEquals() { 172 | // Arrange 173 | MethodSpy spy = new MethodSpy('method'); 174 | spy.returns('anything'); 175 | FakeAsserter fakeAsserter = new FakeAsserter(); 176 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 177 | spy.call( 178 | new List{ Integer.class, String.class, String.class }, 179 | new List{ 'count', 'name', 'otherName' }, 180 | new List{ '3', 'param', 'test' } 181 | ); 182 | 183 | // Act 184 | sut.hasBeenCalledWith('3', 'param', 'test'); 185 | 186 | // Assert 187 | Assert.areEqual(2, fakeAsserter.callCount); 188 | Assert.isFalse(fakeAsserter.failed); 189 | } 190 | 191 | @IsTest 192 | static void given4Arguments_hasBeenCalledWith_callsAssertEquals() { 193 | // Arrange 194 | MethodSpy spy = new MethodSpy('method'); 195 | spy.returns('anything'); 196 | FakeAsserter fakeAsserter = new FakeAsserter(); 197 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 198 | spy.call( 199 | new List{ Integer.class, String.class, String.class, String.class }, 200 | new List{ 'count', 'name', 'otherName', 'yetAnotherName' }, 201 | new List{ '4', 'param', 'unit', 'test' } 202 | ); 203 | 204 | // Act 205 | sut.hasBeenCalledWith('4', 'param', 'unit', 'test'); 206 | 207 | // Assert 208 | Assert.areEqual(2, fakeAsserter.callCount); 209 | Assert.isFalse(fakeAsserter.failed); 210 | } 211 | 212 | @IsTest 213 | static void given5Arguments_hasBeenCalledWith_callsAssertEquals() { 214 | // Arrange 215 | MethodSpy spy = new MethodSpy('method'); 216 | spy.returns('anything'); 217 | FakeAsserter fakeAsserter = new FakeAsserter(); 218 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 219 | spy.call( 220 | new List{ Integer.class, String.class, String.class, String.class, String.class }, 221 | new List{ 'count', 'name', 'otherName', 'yetAnotherName', 'latestArgument' }, 222 | new List{ '5', 'param', 'unit', 'test', 'scenario' } 223 | ); 224 | 225 | // Act 226 | sut.hasBeenCalledWith('5', 'param', 'unit', 'test', 'scenario'); 227 | 228 | // Assert 229 | Assert.areEqual(2, fakeAsserter.callCount); 230 | Assert.isFalse(fakeAsserter.failed); 231 | } 232 | 233 | @IsTest 234 | static void givenMatchables_hasBeenCalledWith_callsAssertEquals() { 235 | // Arrange 236 | MethodSpy spy = new MethodSpy('method'); 237 | spy.returns('anything'); 238 | FakeAsserter fakeAsserter = new FakeAsserter(); 239 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 240 | spy.call(new List{ String.class }, new List{ 'name' }, new List{ 'param' }); 241 | 242 | // Act 243 | sut.hasBeenCalledWith(Argument.equals('param')); 244 | 245 | // Assert 246 | Assert.areEqual(2, fakeAsserter.callCount); 247 | Assert.isFalse(fakeAsserter.failed); 248 | } 249 | 250 | @IsTest 251 | static void givenNullParam_hasBeenCalledWith_callsAssertEquals() { 252 | // Arrange 253 | MethodSpy spy = new MethodSpy('method'); 254 | FakeAsserter fakeAsserter = new FakeAsserter(); 255 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 256 | spy.call(new List{}, new List{}, new List{}); 257 | 258 | // Act 259 | sut.hasBeenCalledWith(null); 260 | 261 | Assert.areEqual(2, fakeAsserter.callCount); 262 | Assert.isTrue(fakeAsserter.failed); 263 | Assert.areEqual('Method method was not called with (null)\nmethod call history:\n\t#1 method(())\n', fakeAsserter.errorMessage); 264 | } 265 | 266 | @IsTest 267 | static void givenNullParam_hasBeenLastCalledWith_callsAssertEquals() { 268 | // Arrange 269 | MethodSpy spy = new MethodSpy('method'); 270 | FakeAsserter fakeAsserter = new FakeAsserter(); 271 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 272 | 273 | // Act 274 | sut.hasBeenLastCalledWith(null); 275 | 276 | // Assert 277 | Assert.areEqual(2, fakeAsserter.callCount); 278 | Assert.isTrue(fakeAsserter.failed); 279 | Assert.areEqual('Method method was not last called with (null)', fakeAsserter.errorMessage); 280 | } 281 | 282 | @IsTest 283 | static void givenObject_hasBeenLastCalledWith_callsAssertEquals() { 284 | // Arrange 285 | MethodSpy spy = new MethodSpy('method'); 286 | FakeAsserter fakeAsserter = new FakeAsserter(); 287 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 288 | 289 | // Act 290 | sut.hasBeenLastCalledWith(new Account()); 291 | 292 | // Assert 293 | Assert.areEqual(2, fakeAsserter.callCount); 294 | Assert.isTrue(fakeAsserter.failed); 295 | Assert.areEqual('Method method was not last called with (Account:{})', fakeAsserter.errorMessage); 296 | } 297 | 298 | @IsTest 299 | static void givenMixList_hasBeenLastCalledWith_callsAssertEquals() { 300 | // Arrange 301 | MethodSpy spy = new MethodSpy('method'); 302 | FakeAsserter fakeAsserter = new FakeAsserter(); 303 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 304 | 305 | // Act 306 | sut.hasBeenLastCalledWith(new Account(), Argument.equals('test')); 307 | 308 | // Assert 309 | Assert.areEqual(2, fakeAsserter.callCount); 310 | Assert.isTrue(fakeAsserter.failed); 311 | Assert.areEqual('Method method was not last called with (Account:{}, test)', fakeAsserter.errorMessage); 312 | } 313 | 314 | @IsTest 315 | static void givenNoParameter_hasBeenLastCalledWith_callsAssertEquals() { 316 | // Arrange 317 | MethodSpy spy = new MethodSpy('method'); 318 | FakeAsserter fakeAsserter = new FakeAsserter(); 319 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 320 | 321 | // Act 322 | sut.hasBeenLastCalledWith(); 323 | 324 | // Assert 325 | Assert.areEqual(2, fakeAsserter.callCount); 326 | Assert.isTrue(fakeAsserter.failed); 327 | Assert.areEqual('Method method was not last called with ()', fakeAsserter.errorMessage); 328 | } 329 | 330 | @IsTest 331 | static void given1Arguments_hasBeenLastCalledWith_callsAssertEquals() { 332 | // Arrange 333 | MethodSpy spy = new MethodSpy('method'); 334 | spy.returns('anything'); 335 | FakeAsserter fakeAsserter = new FakeAsserter(); 336 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 337 | spy.call(new List{ String.class }, new List{ 'name' }, new List{ 'param' }); 338 | 339 | // Act 340 | sut.hasBeenLastCalledWith(Argument.of('param')); 341 | 342 | // Assert 343 | Assert.areEqual(2, fakeAsserter.callCount); 344 | Assert.isFalse(fakeAsserter.failed); 345 | } 346 | 347 | @IsTest 348 | static void given2Arguments_hasBeenLastCalledWith_callsAssertEquals() { 349 | // Arrange 350 | MethodSpy spy = new MethodSpy('method'); 351 | spy.returns('anything'); 352 | FakeAsserter fakeAsserter = new FakeAsserter(); 353 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 354 | spy.call(new List{ Integer.class, String.class }, new List{ 'count', 'name' }, new List{ '2', 'param' }); 355 | 356 | // Act 357 | sut.hasBeenLastCalledWith('2', 'param'); 358 | 359 | // Assert 360 | Assert.areEqual(2, fakeAsserter.callCount); 361 | Assert.isFalse(fakeAsserter.failed); 362 | } 363 | 364 | @IsTest 365 | static void given3Arguments_hasBeenLastCalledWith_callsAssertEquals() { 366 | // Arrange 367 | MethodSpy spy = new MethodSpy('method'); 368 | spy.returns('anything'); 369 | FakeAsserter fakeAsserter = new FakeAsserter(); 370 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 371 | spy.call( 372 | new List{ Integer.class, String.class, String.class }, 373 | new List{ 'count', 'name', 'anotherName' }, 374 | new List{ '3', 'param', 'test' } 375 | ); 376 | 377 | // Act 378 | sut.hasBeenLastCalledWith('3', 'param', 'test'); 379 | 380 | // Assert 381 | Assert.areEqual(2, fakeAsserter.callCount); 382 | Assert.isFalse(fakeAsserter.failed); 383 | } 384 | 385 | @IsTest 386 | static void given4Arguments_hasBeenLastCalledWith_callsAssertEquals() { 387 | // Arrange 388 | MethodSpy spy = new MethodSpy('method'); 389 | spy.returns('anything'); 390 | FakeAsserter fakeAsserter = new FakeAsserter(); 391 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 392 | spy.call( 393 | new List{ Integer.class, String.class, String.class, String.class }, 394 | new List{ 'count', 'name', 'anotherName', 'yetAnotherName' }, 395 | new List{ '4', 'param', 'unit', 'test' } 396 | ); 397 | 398 | // Act 399 | sut.hasBeenLastCalledWith('4', 'param', 'unit', 'test'); 400 | 401 | // Assert 402 | Assert.areEqual(2, fakeAsserter.callCount); 403 | Assert.isFalse(fakeAsserter.failed); 404 | } 405 | 406 | @IsTest 407 | static void given5Arguments_hasBeenLastCalledWith_callsAssertEquals() { 408 | // Arrange 409 | MethodSpy spy = new MethodSpy('method'); 410 | spy.returns('anything'); 411 | FakeAsserter fakeAsserter = new FakeAsserter(); 412 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 413 | spy.call( 414 | new List{ Integer.class, String.class, String.class, String.class, String.class }, 415 | new List{ 'count', 'name', 'anotherName', 'yetAnotherName', 'lastParameter' }, 416 | new List{ '5', 'param', 'unit', 'test', 'scenario' } 417 | ); 418 | 419 | // Act 420 | sut.hasBeenLastCalledWith('5', 'param', 'unit', 'test', 'scenario'); 421 | 422 | // Assert 423 | Assert.areEqual(2, fakeAsserter.callCount); 424 | Assert.isFalse(fakeAsserter.failed); 425 | } 426 | 427 | @IsTest 428 | static void givenMatchables_hasBeenLastCalledWith_callsAssertEquals() { 429 | // Arrange 430 | MethodSpy spy = new MethodSpy('method'); 431 | spy.returns('anything'); 432 | FakeAsserter fakeAsserter = new FakeAsserter(); 433 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 434 | spy.call(new List{ String.class }, new List{ 'name' }, new List{ 'param' }); 435 | 436 | // Act 437 | sut.hasBeenLastCalledWith(Argument.equals('param')); 438 | 439 | // Assert 440 | Assert.areEqual(2, fakeAsserter.callCount); 441 | Assert.isFalse(fakeAsserter.failed); 442 | } 443 | 444 | @IsTest 445 | static void givenNullMatchables_hasBeenLastCalledWithNull_callsAssertEquals() { 446 | // Arrange 447 | MethodSpy spy = new MethodSpy('method'); 448 | FakeAsserter fakeAsserter = new FakeAsserter(); 449 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 450 | spy.call(new List{}, new List{}, new List()); 451 | 452 | // Act 453 | sut.hasBeenLastCalledWith(null); 454 | 455 | Assert.areEqual(2, fakeAsserter.callCount); 456 | Assert.isTrue(fakeAsserter.failed); 457 | Assert.areEqual('Method method was not last called with (null)\nmethod call history:\n\t#1 method(())\n', fakeAsserter.errorMessage); 458 | } 459 | 460 | @IsTest 461 | static void givenCall_hasBeenCalledTimes_returnsCalledTimes() { 462 | // Arrange 463 | MethodSpy spy = new MethodSpy('method'); 464 | FakeAsserter fakeAsserter = new FakeAsserter(); 465 | Expect.MethodSpyExpectable sut = Expect.that(spy, fakeAsserter); 466 | spy.call(new List{}, new List{}, new List{}); 467 | 468 | // Act & Assert 469 | sut.hasBeenCalledTimes(1); 470 | Assert.isFalse(fakeAsserter.failed); 471 | 472 | sut.hasBeenCalledTimes(0); 473 | Assert.isTrue(fakeAsserter.failed); 474 | Assert.areEqual('Method method was not called 0 times\nmethod call history:\n\t#1 method(())\n', fakeAsserter.errorMessage); 475 | 476 | sut.hasBeenCalledTimes(2); 477 | Assert.isTrue(fakeAsserter.failed); 478 | Assert.areEqual('Method method was not called 2 times\nmethod call history:\n\t#1 method(())\n', fakeAsserter.errorMessage); 479 | } 480 | 481 | @IsTest 482 | static void givenExpectedEqualsAssert_MethodSpyAsserter_doNotThrow() { 483 | // Arrange 484 | Expect.Asserter sut = new Expect.MethodSpyAsserter(); 485 | 486 | // Act 487 | sut.isTrue(true, new FakeErrorMessage('it works')); 488 | sut.isFalse(false, new FakeErrorMessage('it works too')); 489 | 490 | // Assert 491 | Assert.isTrue(true, 'This assertions should be reached'); 492 | } 493 | 494 | class FakeErrorMessage implements Expect.ErrorMessage { 495 | private String message; 496 | FakeErrorMessage(String message) { 497 | this.message = message; 498 | } 499 | public override String toString() { 500 | return message; 501 | } 502 | } 503 | 504 | class FakeAsserter implements Expect.Asserter { 505 | public Integer callCount = 0; 506 | public Boolean failed = false; 507 | public String errorMessage; 508 | 509 | public void isTrue(Boolean value, Expect.ErrorMessage message) { 510 | this.callCount++; 511 | if (!value) { 512 | this.failed = true; 513 | this.errorMessage = message.toString(); 514 | } 515 | } 516 | 517 | public void isFalse(Boolean value, Expect.ErrorMessage message) { 518 | this.callCount++; 519 | if (value) { 520 | this.failed = true; 521 | this.errorMessage = message.toString(); 522 | } 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/ExpectTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyOnceTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | @IsTest 9 | private class MethodSpyOnceTest { 10 | @IsTest 11 | static void givenSpyConfiguredToGloballyReturnsOnce_whenCalledOnce_returnsConfiguration() { 12 | // Arrange 13 | MethodSpy sut = new MethodSpy('methodName'); 14 | sut.returnsOnce('global'); 15 | 16 | // Act 17 | Object resultMatcher = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 18 | Assert.areEqual('global', resultMatcher); 19 | 20 | // Assert 21 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 22 | Assert.isNull(result); 23 | } 24 | 25 | @IsTest 26 | static void givenSpyConfiguredToGloballyThrowsOnce_whenCalledOnce_throwsConfiguration() { 27 | // Arrange 28 | MethodSpy sut = new MethodSpy('methodName'); 29 | sut.throwsExceptionOnce(new IllegalArgumentException('global')); 30 | 31 | // Act 32 | try { 33 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 34 | Assert.fail('it shoud not reach this line'); 35 | } catch (Exception e) { 36 | Assert.areEqual('global', e.getMessage()); 37 | } 38 | 39 | // Assert 40 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 41 | Assert.isNull(result); 42 | } 43 | 44 | @IsTest 45 | static void givenSpyConfiguredToMatchReturnsOnce_whenCalledOnce_returnsConfiguration() { 46 | // Arrange 47 | MethodSpy sut = new MethodSpy('methodName'); 48 | sut.whenCalledWith('str').thenReturnOnce('match'); 49 | 50 | // Act 51 | Object resultMatcher = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 52 | Assert.areEqual('match', resultMatcher); 53 | 54 | // Assert 55 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 56 | Assert.isNull(result); 57 | } 58 | 59 | @IsTest 60 | static void givenSpyConfiguredToMatchThrowsOnce_whenCalledOnce_throwsConfiguration() { 61 | // Arrange 62 | MethodSpy sut = new MethodSpy('methodName'); 63 | sut.whenCalledWith('str').thenThrowOnce(new IllegalArgumentException('match')); 64 | 65 | // Act 66 | try { 67 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 68 | Assert.fail('it shoud not reach this line'); 69 | } catch (Exception e) { 70 | Assert.areEqual('match', e.getMessage()); 71 | } 72 | 73 | // Assert 74 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 75 | Assert.isNull(result); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyOnceTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyTestCustomBehavior.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | @IsTest 9 | private class MethodSpyTestCustomBehavior { 10 | class ReturnParam implements MethodSpy.SpyBehavior { 11 | public Object execute(final List params) { 12 | return params; 13 | } 14 | } 15 | 16 | @IsTest 17 | static void givenSpyConfiguredWithBehavior_whenCalled_thenExecuteTheBehavior() { 18 | // Arrange 19 | MethodSpy sut = new MethodSpy('methodName'); 20 | sut.behaves(new ReturnParam()); 21 | final String valueParameter = 'str'; 22 | 23 | // Act 24 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 25 | 26 | // Assert 27 | Assert.areEqual(new List{ valueParameter }, result); 28 | Assert.isTrue(valueParameter === result[0]); 29 | } 30 | 31 | @IsTest 32 | static void givenSpyConfiguredWithMatcherWithBehavior_whenCalledWithMatching_thenExecuteTheBehavior() { 33 | // Arrange 34 | MethodSpy sut = new MethodSpy('methodName'); 35 | sut.whenCalledWith('str').thenBehave(new ReturnParam()); 36 | final String valueParameter = 'str'; 37 | 38 | // Act 39 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 40 | 41 | // Assert 42 | Assert.areEqual(new List{ valueParameter }, result); 43 | Assert.isTrue(valueParameter === result[0]); 44 | } 45 | 46 | @IsTest 47 | static void givenSpyConfiguredWithBehaviorOnce_whenCalled_thenExecuteTheBehaviorOnce() { 48 | // Arrange 49 | MethodSpy sut = new MethodSpy('methodName'); 50 | sut.behavesOnce(new ReturnParam()); 51 | final String valueParameter = 'str'; 52 | 53 | // Act 54 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 55 | Assert.areEqual(new List{ valueParameter }, result); 56 | Assert.isTrue(valueParameter === result[0]); 57 | 58 | final Object secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 59 | Assert.isNull(secondResult); 60 | } 61 | 62 | @IsTest 63 | static void givenSpyConfiguredWithMatcherWithBehaviorOnce_whenCalledWithMatching_thenExecuteTheBehaviorOnce() { 64 | // Arrange 65 | MethodSpy sut = new MethodSpy('methodName'); 66 | sut.whenCalledWith('str').thenBehaveOnce(new ReturnParam()); 67 | final String valueParameter = 'str'; 68 | 69 | // Act 70 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 71 | Assert.areEqual(new List{ valueParameter }, result); 72 | Assert.isTrue(valueParameter === result[0]); 73 | 74 | final Object secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 75 | Assert.isNull(secondResult); 76 | } 77 | 78 | @IsTest 79 | static void givenSpyConfiguredWithBehaviorTimes_whenCalled_thenExecuteTheBehaviorTimes() { 80 | // Arrange 81 | MethodSpy sut = new MethodSpy('methodName'); 82 | sut.behaves(new ReturnParam(), 2); 83 | final String valueParameter = 'str'; 84 | 85 | // Act 86 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 87 | Assert.areEqual(new List{ valueParameter }, result); 88 | Assert.isTrue(valueParameter === result[0]); 89 | 90 | Object secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 91 | Assert.areEqual(new List{ valueParameter }, secondResult); 92 | 93 | secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 94 | Assert.isNull(secondResult); 95 | } 96 | 97 | @IsTest 98 | static void givenSpyConfiguredWithMatcherWithBehaviorTimes_whenCalledWithMatching_thenExecuteTheBehaviorTimes() { 99 | // Arrange 100 | MethodSpy sut = new MethodSpy('methodName'); 101 | sut.whenCalledWith('str').thenBehave(new ReturnParam(), 2); 102 | final String valueParameter = 'str'; 103 | 104 | // Act 105 | final List result = (List) sut.call(new List{ String.class }, new List{ 'param' }, new List{ valueParameter }); 106 | Assert.areEqual(new List{ valueParameter }, result); 107 | Assert.isTrue(valueParameter === result[0]); 108 | 109 | Object secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 110 | 111 | secondResult = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 112 | Assert.isNull(secondResult); 113 | } 114 | 115 | @IsTest 116 | static void givenSpyConfiguredWithMutatingBehavior_whenCalledWithObjectAndUpdateAttribute_thenObjectAttributeIsMutated() { 117 | // Arrange 118 | MethodSpy sut = new MethodSpy('methodName'); 119 | final Person aPerson = new Person('Name'); 120 | sut.behaves(new MutateParam()); 121 | 122 | // Act 123 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ aPerson }); 124 | 125 | // Assert 126 | Assert.areEqual('Mutated Name', aPerson.name); 127 | } 128 | 129 | class MutateParam implements MethodSpy.SpyBehavior { 130 | public Object execute(final List params) { 131 | final Person aPerson = (Person) params[0]; 132 | aPerson.name = 'Mutated ' + aPerson.name; 133 | return null; 134 | } 135 | } 136 | 137 | class Person { 138 | public String name { get; set; } 139 | public Person(final String name) { 140 | this.name = name; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyTestCustomBehavior.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyTimesTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | @IsTest 9 | private class MethodSpyTimesTest { 10 | @IsTest 11 | static void givenConfiguredSpy_whenCalled_respectTheOrderOfMatching() { 12 | // Arrange 13 | MethodSpy sut = new MethodSpy('methodName'); 14 | sut.returns('global'); 15 | sut.throwsException(new IllegalArgumentException('global')); // It overrides returns (last global configuration overrides) 16 | sut.returnsOnce('global once'); 17 | sut.throwsExceptionOnce(new IllegalArgumentException('global once')); 18 | sut.whenCalledWith('str') 19 | .thenReturn('match') 20 | .thenThrow(new IllegalArgumentException('match')) // It overrides thenReturn (last matching configuration overrides) 21 | .thenReturnOnce('match once') 22 | .thenThrowOnce(new IllegalArgumentException('match once')) 23 | .thenReturn('match times', 2) 24 | .thenThrow(new IllegalArgumentException('match times'), 2); 25 | 26 | // Act & Assert 27 | // whenCalledWith('str').thenReturnOnce('match once') 28 | Object resultMatcherOnce = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 29 | Assert.areEqual('match once', resultMatcherOnce); 30 | 31 | // whenCalledWith('str').thenThrowOnce(new IllegalArgumentException('match once')) 32 | try { 33 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 34 | Assert.fail('it shoud not reach this line'); 35 | } catch (Exception e) { 36 | Assert.areEqual('match once', e.getMessage()); 37 | } 38 | 39 | // whenCalledWith('str').thenReturn('match times', 2) 40 | for (Integer i = 0; i < 2; ++i) { 41 | Object resultMatcherTimes = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 42 | Assert.areEqual('match times', resultMatcherTimes); 43 | } 44 | 45 | // whenCalledWith('str').thenThrow(new IllegalArgumentException('match times'), 2) 46 | for (Integer i = 0; i < 2; ++i) { 47 | try { 48 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 49 | Assert.fail('it shoud not reach this line: ' + result); 50 | } catch (Exception e) { 51 | Assert.areEqual('match times', e.getMessage()); 52 | } 53 | } 54 | 55 | // .returnsOnce('global once') 56 | Object resultGlobalOnce = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 57 | Assert.areEqual('global once', resultGlobalOnce); 58 | 59 | // .throwsExceptionOnce(new IllegalArgumentException('global once')) 60 | try { 61 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 62 | Assert.fail('it shoud not reach this line'); 63 | } catch (Exception e) { 64 | Assert.areEqual('global once', e.getMessage()); 65 | } 66 | 67 | // whenCalledWith('str').thenThrow(new IllegalArgumentException('match')) => Override thenReturn('match') 68 | try { 69 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 70 | Assert.fail('it shoud not reach this line'); 71 | } catch (Exception e) { 72 | Assert.areEqual('match', e.getMessage()); 73 | } 74 | 75 | // .throwsException(new IllegalArgumentException('global')) => Override returns('global') 76 | try { 77 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ null }); 78 | Assert.fail('it shoud not reach this line'); 79 | } catch (Exception e) { 80 | Assert.areEqual('global', e.getMessage()); 81 | } 82 | } 83 | 84 | @IsTest 85 | static void givenSpyConfigured_whenCallingTheSpyWithOtherArguments_itThrowsMethodSpyConfigurationException() { 86 | // Arrange 87 | MethodSpy sut = new MethodSpy('methodName'); 88 | sut.whenCalledWith('Another Param', true).thenReturn('Expected Result'); 89 | sut.whenCalledWith('Expected First Param', false) 90 | .thenReturn('Another expected Result') 91 | .thenReturnOnce('Another expected Result Once') 92 | .thenReturn('Another expected Result Times', 2); 93 | sut.whenCalledWith('Expected First Param', true) 94 | .thenThrow(new IllegalArgumentException('Yet another expected Result')) 95 | .thenThrowOnce(new IllegalArgumentException('Yet another expected Result Once')) 96 | .thenThrow(new IllegalArgumentException('Yet another expected Result Times'), 2); 97 | try { 98 | // Act 99 | sut.call(new List{ String.class, Boolean.class }, new List{ 'param', 'bool' }, new List{ 'Another Param', false }); 100 | Assert.fail('it shoud not reach this line'); 101 | } catch (MethodSpy.ConfigurationException cex) { 102 | // Assert 103 | Assert.areEqual( 104 | 'No stub value found for a call of methodName(String param[Another Param], Boolean bool[false])\nHere are the configured stubs:\n\twhenCalledWith(Another Param, true).thenReturn(Expected Result)\n\twhenCalledWith(Expected First Param, false).thenReturnOnce(Another expected Result Once).thenReturnOnce(Another expected Result Times).thenReturnOnce(Another expected Result Times).thenReturn(Another expected Result)\n\twhenCalledWith(Expected First Param, true).thenThrowOnce(IllegalArgumentException:[]: Yet another expected Result Once).thenThrowOnce(IllegalArgumentException:[]: Yet another expected Result Times).thenThrowOnce(IllegalArgumentException:[]: Yet another expected Result Times).thenThrow(IllegalArgumentException:[]: Yet another expected Result)', 105 | cex.getMessage() 106 | ); 107 | } 108 | } 109 | 110 | @IsTest 111 | static void givenSpyConfiguredToGloballyReturnsNTimes_whenCalledN1Times_returnsConfiguration() { 112 | // Arrange 113 | MethodSpy sut = new MethodSpy('methodName'); 114 | sut.returns('global', 3); 115 | 116 | // Act 117 | for (Integer i = 0; i < 3; ++i) { 118 | Object resultMatcher = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 119 | Assert.areEqual('global', resultMatcher); 120 | } 121 | 122 | // Assert 123 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 124 | Assert.isNull(result); 125 | } 126 | 127 | @IsTest 128 | static void givenSpyConfiguredToGloballyThrowsNTimes_whenCalledN1Times_throwsConfiguration() { 129 | // Arrange 130 | MethodSpy sut = new MethodSpy('methodName'); 131 | sut.throwsException(new IllegalArgumentException('global'), 3); 132 | 133 | // Act 134 | for (Integer i = 0; i < 3; ++i) { 135 | try { 136 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 137 | Assert.fail('it shoud not reach this line'); 138 | } catch (Exception e) { 139 | Assert.areEqual('global', e.getMessage()); 140 | } 141 | } 142 | 143 | // Assert 144 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 145 | Assert.isNull(result); 146 | } 147 | 148 | @IsTest 149 | static void givenSpyConfiguredToMatchReturnsNTimes_whenCalledN1Times_returnsConfiguration() { 150 | // Arrange 151 | MethodSpy sut = new MethodSpy('methodName'); 152 | sut.whenCalledWith('str').thenReturn('match', 3); 153 | 154 | // Act 155 | for (Integer i = 0; i < 3; ++i) { 156 | Object resultMatcher = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 157 | Assert.areEqual('match', resultMatcher); 158 | } 159 | 160 | // Assert 161 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 162 | Assert.isNull(result); 163 | } 164 | 165 | @IsTest 166 | static void givenSpyConfiguredToMatchThrowsNTimes_whenCalledN1Times_throwsConfiguration() { 167 | // Arrange 168 | MethodSpy sut = new MethodSpy('methodName'); 169 | sut.whenCalledWith('str').thenThrow(new IllegalArgumentException('match'), 3); 170 | 171 | // Act 172 | for (Integer i = 0; i < 3; ++i) { 173 | try { 174 | sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 175 | Assert.fail('it shoud not reach this line'); 176 | } catch (Exception e) { 177 | Assert.areEqual('match', e.getMessage()); 178 | } 179 | } 180 | 181 | // Assert 182 | Object result = sut.call(new List{ String.class }, new List{ 'param' }, new List{ 'str' }); 183 | Assert.isNull(result); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MethodSpyTimesTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MockTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | @IsTest 8 | private class MockTest { 9 | // Integration test 10 | @IsTest 11 | static void givenAType_whenSpyinOnWithReturn_callingTheMethodReturnsTheConfiguredOutput() { 12 | // Arrange 13 | String expected = 'expected'; 14 | Mock mock = Mock.forType(DummyInterface.class); 15 | MethodSpy dummyMethod = mock.spyOn('dummyMethod'); 16 | dummyMethod.returns(expected); 17 | DummyInterface sut = (DummyInterface) mock.stub; 18 | 19 | // Act 20 | String result = sut.dummyMethod(); 21 | 22 | // Assert 23 | Assert.areEqual(expected, result); 24 | } 25 | 26 | @IsTest 27 | static void givenAType_whenSpyinOnWithoutReturn_callingTheMethodReturnsNull() { 28 | // Arrange 29 | Mock mock = Mock.forType(DummyInterface.class); 30 | MethodSpy dummyMethod = mock.spyOn('dummyMethod'); 31 | dummyMethod.returns(null); 32 | 33 | DummyInterface sut = (DummyInterface) mock.stub; 34 | 35 | // Act 36 | Object result = sut.dummyMethod(); 37 | 38 | // Assert 39 | Assert.areEqual(null, result); 40 | } 41 | 42 | // Unit test 43 | @IsTest 44 | static void givenAMethodName_thenSpyOnReturnsMethodSpy() { 45 | // Arrange 46 | String expected = 'methodName'; 47 | Mock sut = Mock.forType(DummyInterface.class); 48 | 49 | // Act 50 | MethodSpy result = sut.spyOn(expected); 51 | 52 | // Assert 53 | Assert.areEqual(expected, result.methodName); 54 | } 55 | 56 | @IsTest 57 | static void givenAMethodName_whenCallingSpyOnMultipleTimes_thenReturnsSameMethodSpyCaseInsensitive() { 58 | // Arrange 59 | String methodName = 'methodName'; 60 | Mock sut = Mock.forType(DummyInterface.class); 61 | MethodSpy expected = sut.spyOn(methodName); 62 | 63 | // Act 64 | MethodSpy result = sut.spyOn(methodName); 65 | 66 | // Assert 67 | Assert.areEqual(expected, result); 68 | Assert.isTrue(expected === result); 69 | 70 | // Act 71 | result = sut.spyOn(methodName.toUpperCase()); 72 | 73 | // Assert 74 | Assert.areEqual(expected, result); 75 | Assert.isTrue(expected === result); 76 | 77 | // Act 78 | result = sut.spyOn(methodName.toLowerCase()); 79 | 80 | // Assert 81 | Assert.areEqual(expected, result); 82 | Assert.isTrue(expected === result); 83 | } 84 | 85 | @IsTest 86 | static void givenAMethodName_whenNotSpiedOn_thenGetSpyReturnsNull() { 87 | // Arrange 88 | String methodName = 'methodName'; 89 | Mock sut = Mock.forType(DummyInterface.class); 90 | 91 | // Act 92 | MethodSpy result = sut.getSpy(methodName); 93 | 94 | // Assert 95 | Assert.areEqual(null, result); 96 | } 97 | 98 | @IsTest 99 | static void givenAMethodName_whenSpiedOn_thenGetSpyReturnsMethodSpy() { 100 | // Arrange 101 | String methodName = 'methodName'; 102 | Mock sut = Mock.forType(DummyInterface.class); 103 | MethodSpy expected = sut.spyOn(methodName); 104 | 105 | // Act 106 | MethodSpy result = sut.getSpy(methodName); 107 | 108 | // Assert 109 | Assert.areEqual(expected, result); 110 | } 111 | 112 | @IsTest 113 | static void givenAType_forTypeReturnsMock() { 114 | // Arrange 115 | Type param = DummyInterface.class; 116 | 117 | // Act 118 | Mock result = Mock.forType(param); 119 | 120 | // Assert 121 | Assert.areNotEqual(null, result); 122 | } 123 | 124 | @IsTest 125 | static void givenNull_forTypeThrows() { 126 | // Arrange 127 | Type param = null; 128 | 129 | // Act 130 | try { 131 | Mock result = Mock.forType(param); 132 | // Assert 133 | Assert.fail('Expected exception was not thrown'); 134 | } catch (Exception ex) { 135 | Assert.areNotEqual(null, ex); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/MockTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/deprecated/DeprecatedMethodSpyTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | @IsTest 9 | private class DeprecatedMethodSpyTest { 10 | @IsTest 11 | static void givenSpyConfiguredOnceToReturnOnMatchingArguments_whenCallingDeprecatedCall_whenCallingTheSpyWithOtherArguments_itThrowsMethodSpyConfigurationException() { 12 | // Arrange 13 | MethodSpy sut = new MethodSpy('methodName'); 14 | sut.whenCalledWith('Another Param', true).thenReturn('Expected Result'); 15 | sut.whenCalledWith('Expected First Param', false).thenReturn('Another expected Result'); 16 | sut.whenCalledWith('Expected First Param', true).thenReturn('Yet another expected Result'); 17 | try { 18 | // Act 19 | sut.call(new List{ 'Another Param', false }); 20 | Assert.fail('it shoud not reach this line'); 21 | } catch (MethodSpy.ConfigurationException cex) { 22 | // Assert 23 | Assert.areEqual( 24 | 'No stub value found for a call of methodName(String [Another Param], Boolean [false])\nHere are the configured stubs:\n\twhenCalledWith(Another Param, true).thenReturn(Expected Result)\n\twhenCalledWith(Expected First Param, false).thenReturn(Another expected Result)\n\twhenCalledWith(Expected First Param, true).thenReturn(Yet another expected Result)', 25 | cex.getMessage() 26 | ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /force-app/test/package/classes/unit/deprecated/DeprecatedMethodSpyTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Apex Mockery", 3 | "private": true, 4 | "version": "2.1.0", 5 | "description": "Salesforce App", 6 | "keyword": [ 7 | "salesforce", 8 | "apex", 9 | "test", 10 | "mock" 11 | ], 12 | "scripts": { 13 | "build": "wireit", 14 | "lint": "npm run lint:apex ; npm run lint:doc", 15 | "lint:apex": "sf scanner run --verbose --target 'force-app/**/*.cls' --format table --category 'apex-mockery' --engine pmd", 16 | "lint:doc": "write-good README.md --parse", 17 | "postinstall": "./postInstall.sh", 18 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 19 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 20 | "precommit": "lint-staged", 21 | "prepush": "npm run lint && npm run build && npm run test", 22 | "test": "wireit" 23 | }, 24 | "wireit": { 25 | "build": { 26 | "command": "sf project deploy start", 27 | "files": [ 28 | "**/*.cls" 29 | ], 30 | "output": [] 31 | }, 32 | "test": { 33 | "command": "sf apex run test --result-format human --output-dir ./tests/apex --test-level RunLocalTests --synchronous --code-coverage --detailed-coverage", 34 | "files": [ 35 | "**/*.cls" 36 | ], 37 | "output": [ 38 | "coverage" 39 | ], 40 | "dependencies": [ 41 | "build" 42 | ] 43 | } 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^19.5.0", 47 | "@commitlint/config-conventional": "^19.5.0", 48 | "@prettier/plugin-xml": "^3.4.1", 49 | "husky": "^9.1.6", 50 | "lint-staged": "^15.2.10", 51 | "prettier": "^3.3.3", 52 | "prettier-plugin-apex": "^2.1.5", 53 | "wireit": "^0.14.9", 54 | "write-good": "^1.0.8" 55 | }, 56 | "lint-staged": { 57 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ 58 | "prettier --write" 59 | ], 60 | "README.md": [ 61 | "write-good --parse" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postInstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo y | sf plugins install @salesforce/sfdx-scanner 4 | sf scanner rule add --language apex --path apex-ruleset.xml -------------------------------------------------------------------------------- /resources/class_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/apex-mockery/320d3a731fad390bdcbb815cc17cb0cc8ad5cb63/resources/class_diagram.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/apex-mockery/320d3a731fad390bdcbb815cc17cb0cc8ad5cb63/resources/logo.png -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app/src", 5 | "default": true, 6 | "package": "Apex Mockery", 7 | "versionNumber": "2.3.0.NEXT", 8 | "versionName": "v2.1.0", 9 | "postInstallUrl": "https://github.com/salesforce/apex-mockery#installation", 10 | "releaseNotesUrl": "https://github.com/salesforce/apex-mockery/releases" 11 | }, 12 | { 13 | "path": "force-app/test/package", 14 | "default": false 15 | }, 16 | { 17 | "path": "force-app/src", 18 | "package": "Apex Mockery NS Test", 19 | "versionNumber": "2.1.0.NEXT", 20 | "default": false 21 | } 22 | ], 23 | "name": "apex-mockery", 24 | "namespace": "mockery", 25 | "sfdcLoginUrl": "https://login.salesforce.com", 26 | "sourceApiVersion": "56.0", 27 | "packageAliases": { 28 | "Apex Mockery": "0HoDn000000kA5aKAE", 29 | "Apex Mockery NS Test": "0HoDn000000kA6JKAU" 30 | } 31 | } 32 | --------------------------------------------------------------------------------