FunctionalDev.MoqHelpers
3.1.1
See the version list below for details.
dotnet add package FunctionalDev.MoqHelpers --version 3.1.1
NuGet\Install-Package FunctionalDev.MoqHelpers -Version 3.1.1
<PackageReference Include="FunctionalDev.MoqHelpers" Version="3.1.1" />
paket add FunctionalDev.MoqHelpers --version 3.1.1
#r "nuget: FunctionalDev.MoqHelpers, 3.1.1"
// Install FunctionalDev.MoqHelpers as a Cake Addin #addin nuget:?package=FunctionalDev.MoqHelpers&version=3.1.1 // Install FunctionalDev.MoqHelpers as a Cake Tool #tool nuget:?package=FunctionalDev.MoqHelpers&version=3.1.1
FunctionalDev.MoqHelpers
Moq Helpers is a library which has been created to aid unit testing.
Please read through the rest of this document to view tools which can help reduce the brittle nature of unit tests, decrease excess code when setting up objects and enhance the clarity of what it is you're testing.
A particular focus of this library is setting up Mock objects (including non-public members) and providing access to non-public members and methods of instantiated objects.
For a complete changelog, please see the ChangeLog below
Table of Contents
- Activators - Creating mock objects with less boilerplate code
- Lazy Mock Activation - Create abstract mocks with challenging constructor arguments.
- Lazy Activation - Create complex objects without providing constructor arguments.
- Container (ActivatedObject) Creation - Encapsulate construction arguments to refer to in tests only if required.
- ActivatedObject.SetupMock - Inline mock setup.
- ActivatedObject.BindMocks - Inline mock binding.
- ActivatedObject.AddToArgumentCollection - Inline argument addition.
- ActivatedObject.SwitchArgument - Inline switch argument.
- Object Proxy - Interact with private/protected members (properties, fields, methods and generic methods)
- Examples
- Extension Methods
- Mock.Setup - Setup mock members with less boilerplate code. Enables the setup of protected methods.
- ILogger.Unwrap - Provides resolved ILogger invocations in a simple class.
- ILogger.Verify - Provides verify methods on ILogger, to be used when verifying a particular log has been invoked.
- IEnumerable{T}.Enumerate - Enumerate a collection without storing the results.
- IEnumerable{T}.SingleOrThrow - Throw a custom exception if a single match is not found.
- Mock.GetInvocations - Extract type cast arguments from a method invocation on a mock.
- Mock.PassThrough - Auto-setup all members (fields, properties and methods) for a mock to a given instance.
- Type.TryMatchInterface - Determines if a type inherits from a given interface.
- Type.MethodSignatureMatch - Can be used to check if a given set of types can be used to invoke a method signature.
- ActivatedObject.SetupOptions - Can be used to setup a
IOptions{TConfiguration}
instance on a givenActivatedObject
. - ActivatedObject.ConvertToInstanceType - Converts from
ActivatedObject{Mock{TActivated}}
toActivatedObject{TActivated}
.
- Full Example
- Chaining Calls on ActivatedObject / ActivatedObject{T}
- Changelog
Activators
Creating instances of objects is central to most unit testing. Activator classes have been provided to reduce the overhead and brittle nature of setting up classes, when it may not be required to provide all arguments for the constructor, if those arguments are not necessary for the particular unit test in focus.
These activators can be used to future-proof the creation of objects, in that future changes to an object constructor will not break existing unit tests, providing that the additional arguments do not affect existing behaviour.
Lazy Mock Activation
A MockActivator
class can be used to create Mock{T}
classes.
This activator has CreateLazy
and CreateNull
methods which allow the creation of Mock{T} classes without needing to provide all/any constructor arguments.
Whilst these methods can be used to provide all arguments, they can also be provided with some or all constructor arguments to enable future-proofing with future constructor signature changes.
Given the following class:
public abstract class Person
{
protected Person(ILogger<Person> logger) { }
}
The following example uses CreateLazy, without arguments, to create a new instance of Mock{Person} without supplying an instance of ILogger{Person}. Note that the Person base type will be supplied new Mock{ILogger {Person}}.Object as a value for ILogger{Person}.
public Mock<Person> CreateLazy()
=> MockActivator.CreateLazy<Person>();
The following example uses CreateNull, without arguments, to create a new instance of Mock{Person} without supplying an instance of ILogger{Person}. Note that the Person base type will be supplied null as a value for ILogger{Person}.
public Mock<Person> CreateNull()
=> MockActivator.CreateNull<Person>();
The following example uses CreateLazy, with arguments, to create a new instance of Mock{Person} supplying an instance of Mock{ILogger{Person}}. Note that the Person base type will be supplied loggerMock.Object as a value for ILogger{Person}.
public Mock<Person> CreateLazyWithMockArgument(Mock<ILogger<Person>> loggerMock)
=> MockActivator.CreateLazy<Person>(loggerMock);
The following example uses CreateLazy, with arguments, to create a new instance of Mock{Person} supplying an instance of ILogger{Person}. Note that the Person base type will be supplied loggerInstance as a value for ILogger{Person}.
public Mock<Person> CreateLazyWithMockArgument(ILogger<Person> loggerInstance)
=> MockActivator.CreateLazy<Person>(loggerInstance);
Lazy Activation
A LazyActivator
class can be used to create instances of concrete classes.
This activator has CreateLazy
and CreateNull
methods which allow the creation of concrete classes without needing to provide all/any constructor arguments.
Whilst these methods can be used to provide all arguments, they can also be provided with some or all constructor arguments to enable future-proofing with future constructor signature changes.
An example can be seen below:
public class Person
{
public Person(ILogger<Person> logger) { }
}
The following example uses CreateLazy, without arguments, to create a new instance of Person without supplying an instance of ILogger{Person}. Note that the Person constructor will be supplied new Mock{ILogger{Person}}.Object as a value for ILogger{Person}.
public Person CreateLazy()
=> LazyActivator.CreateLazy<Person>();
The following example uses CreateNull, without arguments, to create a new instance of Person without supplying an instance of ILogger{Person}. Note that the Person constructor will be supplied null as a value for ILogger{Person}.
public Person CreateNull()
=> LazyActivator.CreateNull<Person>();
The following example uses CreateLazy, with arguments, to create a new instance of Person supplying an instance of Mock{ILogger{Person}}. Note that the Person constructor will be supplied loggerMock.Object as a value for ILogger{Person}.
public Person CreateLazyWithMockArgument(Mock<ILogger<Person>> loggerMock)
=> LazyActivator.CreateLazy<Person>(loggerMock);
The following example uses CreateLazy, with arguments, to create a new instance of Person supplying an instance of ILogger{Person}. Note that the Person constructor will be supplied loggerInstance as a value for ILogger{Person}.
public Person CreateLazyWithMockArgument(ILogger<Person> loggerInstance)
=> LazyActivator.CreateLazy<Person>(loggerInstance);
Container Creation
Container Lazy and Mock activation has been added to provide a means of extracted auto loaded construction objects. Please note that whilst this example uses MockActivator
this is also present in LazyActivator
.
private readonly TestClass _testClass;
private readonly Mock<ILogger<TestClass>> _loggerMock;
private readonly Mock<IWidget> _widgetMock;
private readonly IWidget _widget;
public UnitTest()
{
var container = MockActivator.CreateLazyContainer<TestClass>();
_testClass = container.Instance.Object;
_loggerMock = container.GetArgumentAsMock<ILogger<TestClass>>();
_widgetMock = container.GetArgumentAsMock<IWidget>();
_widget = container.GetArgument<IWidget>();
}
ActivatedObject.SetupMock
Setup mock is a chaining method on activated objects to enable inline mock setups.
LazyActivator.CreateLazyContainer<TestClass>()
.SetupMock<IWidget>(mock => mock.Setup(x => x.Value, "value"));
ActivatedObject.BindMocks
Bind mocks is a chaning method on activated objects to enable inline multi mock setups. For example, when the response from mock B depends on mock A.
LazyActivator.CreateLazyContainer<TestClass>()
.BindMocks((IFactory factory, IInstance instance) => factory.Setup(x => x.GetInstance, () => instance.Object));
ActivatedObject.AddToArgumentCollection
Add to argument collection is a chaining method on activated objects to enable the addition to the argument collection inline. The following will insert a new instance using MockActivator or LazyActivator, whichever is relevant.
LazyActivator.CreateLazyContainer<TestClass>()
.AddToArgumentCollection<IWidget>();
The following will insert the given reference.
LazyActivator.CreateLazyContainer<TestClass>()
.AddToArgumentCollection(new Widget());
ActivatedObject.SwitchArgument
Switch argument is a chaining method on activated objects to enable the switching of arguments in the argument collection, before the object is created (.Instance
called).
LazyActivator.CreateLazyContainer<TestClass>()
.SwitchArgument<IWidget>(new AlternativeWidget());
Object Proxy
The ObjectProxy
class can be used to interact (set/get) private members of a given instance, and can be used to invoke protected/private methods.
Dot separated member access is supported to access members of members recursively. E.g. ObjectProxy.For(person)["Name"."FirstName"]
Static classes can be setup with ObjectProxy.ForStatic
to interact with static classes.
Examples
Given the following class:
public class Person
{
private string Name { set; get; }
private Person(ILogger<Person> logger, string name)
{
Name = name;
}
private string GetName()
=> Name;
private string ToStringFor<T>(T arg)
=> arg.ToString();
}
Creating an instance of Person
can be achieved as shown below.
var person = LazyActivator.CreateLazy<Person>("Fred");
// or
var person = LazyActivator.CreateLazy<Person>();
And members can be managed as shown below.
// Get.
var name = ObjectProxy.For(person)["Name"];
// Set.
ObjectProxy.For(person)["Name"] = "Bob";
Methods can be invoked via the proxy with:
var result = person.InvokeMethod<string>("GetName");
For full examples please see end of this file.
Extension Methods
Mock.Setup
Several extension methods have been provided which enable functional Moq creation (returning Mock{T}
to enable chaining) and to setup private/protected members.
Please note that to resolve the extension methods, it may be required to add the following using statement:
using FunctionalDev.MoqHelpers;
Please also note that the extension methods are not the typical .Setup(...).Returns(...)
format. For simple method setups the format this library provides is:
.Setup(type => type.MethodName, Expression<Func<InputArgs, ReturnArg>>);
Given an example interface:
public interface IPerson
{
string SetName(string name);
int GetAge();
}
The following can be used to set up an IPerson
interface.
var person = new Mock<IPerson>()
.Setup(x => x.SetName, (string name) => "")
.Setup(x => x.GetAge, () => 25);
Please note that this does not provide any filters on the arguments for methods (such as It.Is<T>(Func<T, bool>)
filtering).
ILogger.Unwrap
Mock{ILogger}.Unwrap()
is an extension method which returns a set of resolved invocations on the logger.
This can be used when it is required to manually inspect the invocations of a ILogger
Mock.
For example, given the following code snippet:
var loggerMock = new Mock<ILogger<ExampleClass>>();
loggerMock.Object.LogError("Error log example");
loggerMock.Object.LogDebug("Debug log example");
loggerMock.Unwrap().ToList().ForEach(loggerArguments =>
{
Console.WriteLine($"Level: '{loggerArguments.Level}'");
Console.WriteLine($"Message: '{loggerArguments.Message}'");
Console.WriteLine($"Exception: '{loggerArguments.Exception?.Message}'");
Console.WriteLine();
});
Output:
Level: 'Error'
Message: 'Error log example'
Exception: ''
Level: 'Debug'
Message: 'Debug log example'
Exception: ''
ILogger.Verify
Mock{ILogger{T}}.Verify(...)
are a series of extension methods which can be used to verify that an ILogger
has been invoked for a particular ErrorLog
, Times
and message.
A combination of ErrorLog
, message text, and Times
can be used to ensure that a particular log has been invoked. Additionally, a Func<string, ErrorLog, bool>
can be used to have full control over the verification.
IEnumerable{T}.Enumerate
It is sometimes required during unit testing to ensure that an enumerable set has been enumerated (to ensure the collection is valid) before continuing with any assertions, for example the result of a method target. Call IEnumerable{T}.Enumerate()
to fully iterate through a given collection without assigning references to any of the items.
IEnumerable{T}.SingleOrThrow
IEnumerable{T}.SingleOrThrow
can be used to pull a single object of T
from a given collection. If exactly one object is not found then a custom exception is thrown, as provided in the call.
Mock.GetInvocations
Mock.GetInvocations<T1...T10>(string/MethodInfo)
can be used to return type cast method invocations from a given mock.
Mock.PassThrough
Mock.PassThrough
can be used to auto-setup all members (fields, properties and methods) for a mock to a given instance.
Note that for a mock setup this way it will be possible to override setup methods if required, as well as effectively using a mock to capture invoked members of a given instance.
Type.TryMatchInterface
Type.TryMatchInterface
can be used to determine if a given type inherits from a given interface.
Type.MethodSignatureMatch
Type.MethodSignatureMatch
can be used to compare two type arrays and checks that the second set can be used to invoke a method whose signature matches the first set.
ActivatedObject.SetupOptions
ActivatedObject.SetupOptions
can be used to setup a IOptions{TConfiguration}
instance on a given ActivatedObject
.
ActivatedObject.ConvertToInstanceType
Converts from ActivatedObject{Mock{TActivated}}
to ActivatedObject{TActivated}
.
Full Example
Consider the following classes.
public abstract record FullExampleObjectBase(ILogger<FullExampleObjectBase> Logger)
{
public ILogger<FullExampleObjectBase> Logger { get; } = Logger;
private int _localIntegerValue = 1;
protected virtual string GetName()
=> nameof(FullExampleObjectBase);
private void SetLocalIntegerValue(int newValue)
{
_localIntegerValue = newValue;
}
private int GetLocalIntegerValue()
=> _localIntegerValue;
private int GenericMethod<T>() => 1;
public abstract string GetNameAbstract(string arg);
}
public record FullExampleObject(ILogger<FullExampleObjectBase> Logger)
: FullExampleObjectBase(Logger)
{
public override string GetNameAbstract(string arg)
=> arg;
}
public static class FullExampleStaticClass
{
private static void SetStaticValue(string arg) { }
}
The following test code could be used to interact with these classes.
public class FullExample
{
public FullExampleObject CreateFullExampleObject()
=> LazyActivator.CreateLazy<FullExampleObject>();
public FullExampleObject CreateFullExampleObjectWithArgument()
=> LazyActivator.CreateLazy<FullExampleObject>(Mock.Of<ILogger<FullExampleObjectBase>>());
public void SetPrivate()
{
var proxy = ObjectProxy.For(CreateFullExampleObject());
proxy["_localIntegerValue"] = 5;
}
public void GetPrivate()
{
var proxy = ObjectProxy.For(CreateFullExampleObject());
var value = (int)proxy["_localIntegerValue"];
}
private void CallPrivate()
{
var proxy = ObjectProxy.For(CreateFullExampleObject());
proxy.InvokeMethod("SetLocalIntegerValue", 5);
}
private void CallPrivateWithReturning()
{
var proxy = ObjectProxy.For(CreateFullExampleObject());
int resultAsInt = proxy.InvokeMethod<int>("GetLocalIntegerValue");
object resultAsObject = proxy.InvokeMethod("GetLocalIntegerValue");
}
private void CallGenericPrivateMethod()
{
var proxy = ObjectProxy.For(CreateFullExampleObject());
int resultAsInt = proxy.InvokeGenericMethod<int>("GenericMethod", new[] { typeof(RecordClass) }, Array.Empty<object>());
object resultAsObject = proxy.InvokeGenericMethod("GenericMethod", new[] { typeof(RecordClass) }, Array.Empty<object>());
}
private void ObjectProxyForStaticClass()
{
var proxy = ObjectProxy.ForStatic(typeof(FullExampleStaticClass));
proxy.InvokeMethod("SetStaticValue", "Hello world");
}
private void SetupExtensionMethods()
{
MockActivator.CreateLazy<FullExampleObjectBase>()
// Setting members.
.Setup(x => x.Logger, Mock.Of<ILogger<FullExampleObjectBase>>())
// Setting public methods.
.Setup(x => x.GetNameAbstract, (string arg) => arg + "test")
// Setting private methods.
.Setup("GetName", () => "Hello World")
// Also works for public methods.
.Setup("GetNameAbstract", (string arg) => arg + "test")
;
}
}
Chaining Calls on ActivatedObject
/ActivatedObject{T}
Further changes in containers allows for chaining setups:
var container = LazyActivator.CreateLazyContainer<MyTestClass>()
// Setup mocks can be used to setup mocks in chaining calls.
.SetupMock<IWidget>(widget => { })
.SetupMock<ITestObject>(testObject => testObject.Setup(x => x.Name, "Fred"))
.SetupMock<ITestObject>(testObject => testObject.Setup(x => x.Name, "Frank"))
// Add to argument collection can be used to add arguments into the container.
.AddToArgumentCollection(new Mock<ILogger<MyTestClass>>().Object)
.SetupMock<ILogger<MyTestClass>>(logger =>
{
logger.Setup(...)
})
// Bind mocks can be used to setup between mocks.
.BindMocks<IHttpClient, IHttpClientFactory>((client, factory) => factory.Setup(...));
ChangeLog
3.1.0 | 21/01/2024
- Added to
ActivatedObject
,ActivatedObject{T}
andIArgumentProvider
:SwitchArgument
. Allows substitution of an argument value to another instance.
3.0.25 | 05/12/2023
- Added to
LoggerExtensions
the ability to verify by exception in addition to other verification arguments.
3.0.24 | 04/12/2023
- Added
CallbackHelpers.Throw
to help with throwing exceptions in setup callback arguments.
3.0.23 | 15/11/2023
- Added
ObjectProxyBase.SetPropertyBehaviour
which can be used to configure converting behaviour when assigning properties (defaults false). - Added
TypeExtensions.GetNonNullableType
to use when resolving int? to int or string to string for example.
3.0.22 | 10/10/2023
- Added
Action SequenceBuilder.GenerateSequence(Action[] actionSequence)
- Added
Func<T> SequenceBuilder.GenerateSequence(Func<T>[] funcSequence)
- Added
IActionSequence SequenceBuilder.Build(Action[] actionSequence)
(to be used when tracking invocation count) - Added
IFuncSequence<T> SequenceBuilder.Build(Func<T>[] funcSequence)
(to be used when tracking invocation count)
3.0.21 | 06/10/2023
- Added
.SetupMock(Action<IArgumentProvider,Mock<TMocked>>)
toActivatedObject
andActivatedObject{}
. - Added
SequenceBuilder.GenerateSequence
.
3.0.20 | 07/08/2023
- Updated ObjectProxy selection logic for members and methods to include base types considering read and write permissions in the inheritance tree
3.0.19 | 31/07/2023
- Updated readme
3.0.18 | 31/07/2023
- Added a common
GetInstance
method to ActivatedObjectBase - Fixed
MockActivator.CreateMock(Type,ConstructorOptions,bool,Array{Object})
throwing when attempting to cast ActivatedObject`1 to ActivatedObject
3.0.17 | 27/06/2023
- Extended logging verify calls to handle event IDs.
- Included additional information in logger unwrap.
3.0.16 | 02/06/2023
- Further fix for a bug in
Mock.PassThrough
where an implementation has methods or properties which do not exist on the type being mocked
3.0.15 | 02/06/2023
- Fixed a bug in
Mock.PassThrough
where an implementation has methods or properties which do not exist on the type being mocked
3.0.14 | 15/05/2023
- Added strict construction methods to
LazyActivator
andMockActivator
3.0.13 | 18/04/2023
- Additional fix for bug with
ActivatedObject.SetupMock
when the mock type is a mocked abstract.
3.0.12 | 18/04/2023
- Fixed bug with
ActivatedObject.SetupMock
when the mock type is a mocked abstract. - Added
ActivatedObject{T}.BindMocks
andActivatedObject.BindMocks
- Added
ActivatedObject{T}.AddToArgumentCollection
andActivatedObject.AddToArgumentCollection
3.0.11 | 17/04/2023
- Added SetupOptions
3.0.10 | 14/04/2023
- Rewrote
ActivatedObject
to only create the object onceActivatedObject.Instance
has been invoked (deferred loading). Allows for the arguments, if required, to be configured before the constructor is called..
3.0.9 | 14/04/2023
- Added extensions to readme
3.0.8 | 14/04/2023
- Fixed readme TOC
3.0.7 | 14/04/2023
- Fixed
ActivatedObject.GetArgument<TClass>()
returning null.
3.0.6 | 17/02/2023
- Added ConvertToInstanceType extension to convert from
ActivatedObject<Mock<T>>
toActivatedObject<T>
.
3.0.5 | 16/02/2023
- Fixed a bug in Mock<ILogger>.Verify() where a particular execution path recursively calls itself and causes a stack overflow.
3.0.4 | 07/02/2023
- Fixed a bug in Mock<ILogger>.Unwrap() where a non
Log
method invocation has occurred.
3.0.3 | 07/02/2023
- Added support for .NET 6
3.0.2 | 24/01/2023
- Reversed previous obsolete flag additions
3.0.1 | 11/01/2023
- Introduced ChangeLog
3.0.0 | 11/01/2023
- Breaking -
ActivatedObject{T}
no longer inherits fromActivatedObject
, instead a new base class has been introduced (ActivatedObjectBase
) which they both share. - Breaking - The two methods
GetArgument
andGetArgumentAsMock
have been marked as obsolete (and have been moved toActivatedObjectBase
) and may be removed on the next major release. - Breaking -
ActivatedObject
no longer has a property calledInstanceUnTyped
it has been renamed toInstance
with a type ofobject
, mirroring the property onActivatedObject{T}
namedInstance
of typeT
(class generic type). - Added chaining calls to ActivatedObjects to enable setting up of mocks inline/chained.
- Rewrite on the argument logic allowing custom injection of objects after instantiation.
- Updated all
ILogger{T}
extension methods to also supportILogger
. Logger
/Logger{T}
.Unwrap() now returns the exception, if provided on invocations.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Logging.Abstractions (>= 6.0.1)
- Microsoft.Extensions.Options (>= 6.0.0)
- Moq (>= 4.18.4)
- System.Linq.Expressions (>= 4.3.0)
-
net6.0
- Microsoft.Extensions.Logging.Abstractions (>= 6.0.1)
- Microsoft.Extensions.Options (>= 6.0.0)
- Moq (>= 4.18.4)
- System.Linq.Expressions (>= 4.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
3.2.0 | 162 | 9/17/2024 |
3.1.6 | 79 | 7/24/2024 |
3.1.5 | 126 | 7/8/2024 |
3.1.4 | 174 | 2/20/2024 |
3.1.3 | 152 | 1/22/2024 |
3.1.2 | 97 | 1/22/2024 |
3.1.1 | 107 | 1/21/2024 |
3.1.0 | 95 | 1/21/2024 |
3.0.25 | 221 | 12/6/2023 |
3.0.24 | 142 | 12/4/2023 |
3.0.23 | 187 | 11/15/2023 |
3.0.22 | 205 | 10/10/2023 |
3.0.21 | 140 | 10/6/2023 |
3.0.20 | 202 | 8/7/2023 |
3.0.19 | 157 | 7/31/2023 |
3.0.18 | 144 | 7/31/2023 |
3.0.17 | 174 | 6/27/2023 |
3.0.16 | 192 | 6/2/2023 |
3.0.15 | 148 | 6/2/2023 |
3.0.14 | 179 | 5/15/2023 |
3.0.13 | 259 | 4/18/2023 |
3.0.12 | 187 | 4/18/2023 |
3.0.11 | 180 | 4/17/2023 |
3.0.10 | 206 | 4/14/2023 |
3.0.9 | 188 | 4/14/2023 |
3.0.8 | 198 | 4/14/2023 |
3.0.7 | 192 | 4/14/2023 |
3.0.6 | 295 | 2/17/2023 |
3.0.5 | 240 | 2/16/2023 |
3.0.4 | 266 | 2/9/2023 |
3.0.3 | 268 | 2/7/2023 |
3.0.2 | 1,865 | 1/24/2023 |
3.0.1 | 360 | 1/11/2023 |
3.0.0 | 309 | 1/11/2023 |
2.1.4 | 488 | 11/1/2022 |
2.1.3 | 556 | 7/19/2022 |
2.1.2 | 495 | 5/3/2022 |
2.1.1 | 451 | 5/3/2022 |
2.1.0 | 443 | 4/27/2022 |
2.0.15 | 433 | 4/26/2022 |
2.0.14 | 494 | 3/10/2022 |
2.0.13 | 748 | 1/27/2022 |
2.0.12 | 662 | 1/24/2022 |
2.0.11 | 653 | 1/21/2022 |
2.0.10 | 427 | 9/10/2021 |
2.0.9 | 351 | 9/10/2021 |
2.0.8 | 312 | 9/3/2021 |
2.0.7 | 312 | 9/3/2021 |
2.0.6 | 337 | 9/2/2021 |
2.0.5 | 311 | 8/27/2021 |
2.0.4 | 293 | 8/27/2021 |
2.0.3 | 307 | 8/13/2021 |
2.0.2 | 356 | 7/29/2021 |
2.0.1 | 339 | 7/28/2021 |
2.0.0 | 336 | 7/28/2021 |
1.1.0 | 389 | 7/28/2021 |
1.0.0 | 376 | 7/27/2021 |