Upgrading from Maester 2.x to 3.0
Maester 3.0 gives Maester its own test engine. The built-in tests are no longer Pester files that you copy into a folder: they ship inside the module and run from there. Most runs need no change at all, but the way tests are installed, selected, configured and written has moved on. This page lists every change that can affect you.
Checklist
-
Run Maester on PowerShell 7.4 or later. Windows PowerShell 5.1 is no longer supported.
-
Update the module, and remove Maester 2.x from the machine:
Update-Module Maester -ForceUninstall-Module Maester -MaximumVersion 2.99.99 -AllVersionsStart a new PowerShell session afterwards.
-
Clean up your tests folder:
Update-MaesterTests -Path ./maester-tests -WhatIf, then without-WhatIf. -
If you disabled built-in tests by deleting their files, disable them in
maester-config.jsoninstead. -
If you have Pester-format custom tests, install Pester 5.7.1 or later yourself, or convert the tests with
Convert-MtTest. -
If a custom test calls a built-in
Test-Mt*function, replace the call withInvoke-MtTest -Id <ID>. -
If you select tests with
-Tag, your tags still work. Move to-Product,-Feature,-Area,-Severityand-Suitewhen you can; see Tags. -
If you read the result JSON or XML, review the result changes.
BlockandTagof the built-in tests changed. -
Try your usual command with
-DryRun -PassThrufirst to see what would run.
Requirements
- PowerShell 7.4 or later (the
Coreedition) on Windows, Linux or macOS. The module no longer loads in Windows PowerShell 5.1. - Pester is optional. Maester no longer depends on Pester or installs it. You need Pester 5.7.1 or later only
if you run Pester-format custom tests, or if your scripts call
New-PesterConfiguration. Without Pester, a run with Pester-format tests reports them asErrorrows with reasonPesterNotAvailableand still runs everything else. - Remove Maester 2.x.
Update-Modulekeeps the old version installed. When a script calls a function that only 2.x exports, PowerShell loads 2.x into the session and from then onInvoke-Maesteris the 2.x one. Maester 3.0 warns when 2.x is installed, and if 2.x gets loaded during a run it removes it again and marks the affected rowErrorwith reasonForeignModuleLoaded. After updating, start a new session: a loaded module cannot be replaced in a running process.
The built-in tests run from the module
In 2.x, Install-MaesterTests copied the tests into a folder and Update-MaesterTests refreshed the copies. In
3.0 the tests ship in the module, so updating the module updates the tests. Your folder now holds only your
own tests and your configuration.
-
The built-in tests always run.
-Pathis now "where my custom tests and config live". Commands documented for 2.x, such asInvoke-Maester -Path ./tests/MaesterorInvoke-Maester ./tests/Custom, now run every built-in test plus the custom tests under that path. To run only your custom tests, add-SkipBuiltIn(or setSelection.BuiltIntoNone). To run only some built-in tests, use-Product,-Suiteand the other selectors below, or-TestId. -
A run needs no test folder. Without
-Path, Maester looks for custom tests in the current folder only when it looks like a Maester folder (amaester-config*.jsonfile, aCustomfolder, a 2.x suite folder, or a test file). Otherwise it runs the built-in tests and tells you to pass-Pathfor custom tests. -
A
-Paththat does not exist is a warning, not an error: the built-in tests still run and the config is looked up from the nearest existing parent folder. With-SkipBuiltInit stays an error. -
Deleting a test file no longer disables a test. The built-in tests come back. Disable a test in the config instead:
{ "TestSettings": [ { "Id": "MT.1005", "Enabled": false, "Reason": "Handled by another control" } ] } -
Stale copies are not run. A 2.x copy of a built-in test (identified by its ID, a previous ID, the ID of a retired test, or a family prefix such as
MT.1024.) is skipped wherever it is, including underCustom/. It produces no row; Maester warns once, names the files, and lists them underSelection.Supersededin the result. -
Copying a built-in test into
Custom/to change it no longer works. Change its severity or parameters in the config, or copy it under your own ID and disable the built-in.
Install-MaesterTests and Update-MaesterTests
| Command | 2.x | 3.0 |
|---|---|---|
Install-MaesterTests | Copied every test into the folder and installed Pester. | Writes custom/README.md and a starter maester-config.json when they are missing. Never writes a test file and never overwrites a file, so pipelines can keep calling it on every run. Does not install Pester; -SkipPesterCheck has no effect. |
Update-MaesterTests | Overwrote the test folders with the latest tests. | Removes the 2.x copies of built-in tests: every *.Tests.ps1 outside Custom/ whose tests all have a current, previous or retired built-in ID, and the folders that leaves empty. Keeps files that also hold tests of your own (with a warning), and never touches Custom/. Asks for confirmation unless you pass -Force; supports -WhatIf. It also reduces a copy of the 2.x shipped maester-config.json to the rows that differ from the built-in severities or set something else. |
Pipelines that clone a copy of the 2.x tests and pass its folder to -Path keep working: the copies are
recognised and skipped, and the built-in tests run from the module.
Configuration
The maester-config.json format is the same, with new sections. See the
run configuration reference.
- The shipped rows are gone. The module no longer ships a
maester-config.jsonwith a row per test: each test now carries its default severity. Your config needs only the rows you change. A copy of the 2.x shipped file in your folder (300 or more rows withTitle) still overrides every severity; Maester warns about it.Update-MaesterTestsreduces it to the rows you changed, or delete the others yourself. - Severity comes from the test. The order is: a
TestSettingsrow in your config, then the test'sSeverity, then (for tests that compute it at run time) the severity the test reports.Severity:<level>tags apply only to Pester-format tests. TestSettingsrows do more. BesidesSeverity, a row can setEnabled,Reason,ParametersandTimeoutSeconds, for any test ID. In 2.x onlySeveritywas applied, and a row inCustom/maester-config.jsonworked only if the main file had a row for the same ID.- The tenant file is merged.
maester-config.<tenantId>.jsonused to replacemaester-config.json; now it merges over it (and overCustom/maester-config.json), so a tenant inherits base settings it does not set. A tenant file that relied on hiding a base value must set it explicitly. Maester warns once and names the inherited settings. Custom/maester-config.jsonworks without a root file.- New ways to supply config:
-Config <path | object | array>and theMAESTER_CONFIGenvironment variable. Both make the run hermetic: config files next to the tests are not read.
Selecting tests
New Invoke-Maester parameters:
| Parameter | Effect |
|---|---|
-Product | Run only the tests of these products: keys such as entra or exchange, or an alias such as Entra ID. |
-Feature | Run only the tests about these features of a product, for example conditional-access. |
-Area | Run only the tests of these security areas, for example authentication or privileged-access. |
-Severity | Run only the tests with these severities. |
-Suite | Run only the tests of these suites, for example cisa or eidsca. |
-TestId | Run only these test IDs. Exact IDs or * wildcards, case-insensitive. A test named by its exact ID runs even if it is preview or long-running. |
-ExcludeTestId | Do not run these IDs. Wins over -TestId. |
-Config | The run configuration, instead of the files next to the tests. |
-DryRun | Work out what would run, and why the rest would not, without running any test. Each test that would have run is NotRun with reason DryRun. |
-SkipBuiltIn | Run only the custom tests under -Path. |
Each of the five selectors takes a list. The values of one selector are alternatives, different selectors must
all match, and they combine the same way with -Tag and -TestId:
Invoke-Maester -Product entra, exchange -Severity Critical, High runs the critical and high tests of both
products. The same keys work in the Selection section of the config. See
Selection.
Every existing parameter stays. Changed behaviour:
-Tag Alland-Tag Fullare removed. They were deprecated in 2.x and selected nothing, because no test carries those tags. Using them now stops the run with an error: use-IncludePreviewinstead ofAlland-IncludeLongRunninginstead ofFull.-PesterConfigurationaccepts a[PesterConfiguration]object or a hashtable, and itsRun.Path,Filter.Tag,Filter.ExcludeTagandTestResultoptions apply to native tests too. ItsFilter.ExcludeTagis now added to the default exclusions; 2.x discarded it whenever Preview or long-running tests were excluded by default, so those tests ran. See Pester-format tests.- Preview tests are chosen by an attribute. A built-in test says
Lifecycle = 'Preview'instead of carrying thePreviewtag. The rule is the same: it runs with-IncludePreview, with any-Tag, or when its exact ID is in-TestId. - Active Directory tests are chosen by their service. A test that needs Active Directory runs only after
Connect-Maester -Service ActiveDirectory. TheADtag no longer decides this. - Deselected tests carry a reason.
NotRunrows say why: not selected, excluded by tag or ID, disabled, preview, long-running, Active Directory not connected. See Applicability and reason codes. -Verbosity Normalprints one line per finished test, andDetailedalso a line when each test starts.
Tags
In 2.x a tag could mean a suite, a product, a topic, a benchmark control, a licence or a severity. In 3.0 each of those is an attribute of the test, and the tests that ship with Maester carry no tags for them.
Your -Tag and -ExcludeTag values keep working. Every 2.x tag selects the built-in tests that carried it
in 2.x, and a built-in test's ID is still one of its tags. A run that selects through a 2.x tag writes one
warning that names the tag and, where there is one, the selector that replaces it. Tests added after 3.0 do not
carry 2.x tags, so a tag no longer grows with the product; a selector does.
| 2.x | 3.0 |
|---|---|
-Tag CISA, -Tag Maester | -Suite cisa, -Suite maester |
-Tag EXO, -Tag Entra | -Product exchange, -Product entra |
-Tag CA, -Tag Privileged | -Feature conditional-access, -Area privileged-access |
-Tag MS.AAD.3.5 | -TestId CISA.MS.AAD.3.5 |
-Tag Severity:High | -Severity High |
-Tag MT.1068 | -TestId MT.1068 |
Tag remains for your own tests: a label in the Tag of a [MaesterTest] attribute, or in the Tags of a
suite.json, is matched by -Tag and -ExcludeTag. Pester-format tests are selected by their Pester tags as
before.
Exported commands
Maester 2.x exported about 730 commands, most of them the check functions behind each test (Test-MtCa...,
Test-MtCisa..., Test-MtEidsca...). Maester 3.0 exports 67. The check functions still exist inside the module,
but they are no longer exported and no longer check their own connections or catch their own errors: the engine
does that.
-
To run a built-in check from a script or from a custom test, use
Invoke-MtTest -Id <ID>. It runs the check through the engine, with its connection and licence checks, and returns the result row.# 2.x$ok = Test-MtCaMfaForRiskySignIn# 3.0$ok = (Invoke-MtTest -Id MT.1012).Result -eq 'Passed' -
Test-MtConnectionandTest-MtConditionalAccessWhatIfstay exported with the same parameters. -
The supporting commands (
Connect-Maester,Invoke-MtGraphRequest,Add-MtTestResultDetail,Get-MtLicenseInformation,Get-MtConditionalAccessPolicy, the report and export commands, and so on) stay. -
New commands:
Invoke-MtTest,Get-MtTest,New-MtTest,Convert-MtTest,Get-MtSetting(with the aliasGet-MtMaesterConfigGlobalSetting) andGet-MtTenantContext.Get-MtTestInventoryremains.
Custom tests
- Pester-format custom tests keep running, with Pester 5.7.1 or later installed. Their rows appear in the same report. See Pester-format tests.
- New tests should be native tests:
Test.<ID>.ps1with a[MaesterTest(...)]attribute plusTest.<ID>.md, no connection checks, notry/catch. See Writing native tests. Convert-MtTest -Path ./Customconverts Pester-format tests to native tests and reports what needs a manual touch. TheDescribename becomes the test'sFeature, so theBlockof its result rows and the name of its test case in CI stay what they were.- Native tests written for a 3.0 preview may use
CategoryorPreviewin the attribute. Both were removed: such a test is anErrorrow with reasonInvalidMetadataand a message that names the replacement. UseProductandFeatureinstead ofCategory(to keep the row'sBlock, put the oldCategorytext inFeature), andLifecycle = 'Preview'instead ofPreview. A custom native test is no longer selected by-Tag <its ID>; use-TestId. - Name your Pester tests
<ID>: <title>so that they can be selected with-TestIdand configured by ID.
Result changes
The result JSON keeps its 2.x shape; the new fields are additions (SchemaVersion is 2.1). See the
result schema. Each row now says what the test is about in typed fields (Product,
RelatedProduct, Feature, Area, ResourceType, UserImpact, ImplementationCost, Mitre, Control,
Lifecycle, ReplacedBy, Service, License, Permission), and the top-level Taxonomy gives the names of
the keys. These rows can come out differently from 2.x:
- A built-in test that throws is
Error, notFailed. In 2.x most exceptions that escaped a check were reported asFailed. A native test has no assertion, so an exception is never a verdict.FailedCountcan go down andErrorCountup. As in 2.x, such errors do not fail the run or the CI test file unless you setOutput.ErrorsAsFailures. - A test that returns no verdict is
Skippedwith reasonNoResult. Tests that decide at run time that they do not apply areSkippedwith reasonNotApplicable(in 2.x some of these werePassed). - A test whose service is not connected is
Skippedwith reasonServiceNotConnected, including the few checks that had no connection check in 2.x and reportedFailedorError. - A family that did not run is one row on the family's ID (for example
MT.1024), withParentIdset. 2.x emitted oneNotRunrow per instance when filtered, and no row when there was nothing to check. - Drift checks are renamed from
MT1060.<folder>.<n>toMT.1060.<folder>.<n>(the folder name is made safe for an ID). The oldMT1060.*tags are kept, so-Tag MT1060still selects them. HelpUrlis filled for every built-in test; 2.x left it empty for many.- A custom test file that fails to load is reported as an
Errorrow with reasonLoadFailed; a Pester file that failed discovery used to produce no rows. - The top-level
ResultisFailedwhen any row failed or when Maester itself raised anErrorrow (an invalid test or configuration, a file that did not load, a duplicate ID), not when a test merely threw. Blockof a built-in test is the name of its product, for exampleEntra ID, where 2.x had theDescribename, for exampleMaester/Entra.Blockstherefore has one entry per product. Pester-format tests keep theirDescribeorContextname.Tagof a built-in test no longer lists suite, product or benchmark tags, or the test ID. It lists only tags written in the test, and the built-in tests have none. ReadSuite,Product,Feature,AreaandSeverityinstead. A family instance keeps its own tags (for example the recommendation type of anMT.1024row).- Maester writes the NUnit/JUnit XML for native and Pester tests together, with outcome rules that keep
pipelines failing on what they failed on before. Test cases are still named
<Block>.<Name>, so the names of the built-in tests change once with theirBlock(Entra ID.MT.1001: ...), and their history in a CI system starts again; the names of Pester-format tests do not change. See CI test results.
CI pipelines
- Pipelines that call
New-PesterConfigurationbeforeInvoke-Maesterneed Pester installed. Either install it as a step (Install-Module Pester -MinimumVersion 5.7.1 -Force -Scope CurrentUser), or pass a hashtable to-PesterConfiguration, or move the settings to the run configuration (Selection,Output.TestResult). - Pipelines that run
Install-MaesterTestsand thenInvoke-Maester -Path <that folder>keep working; the folder now holds only the README and config template. - Pipelines that pass
-Pathto a folder of 2.x test copies now run the built-in tests from the module and skip the copies. Add-SkipBuiltInonly if you want your custom tests alone. - Use
-ConfigorMAESTER_CONFIGto give a pipeline its own configuration without touching the repository's files.