Coder Social home page Coder Social logo

ppi-ag / deep-sampler Goto Github PK

View Code? Open in Web Editor NEW
11.0 5.0 2.0 3.82 MB

DeepSampler is a stubbing framework for compound tests

Home Page: https://ppi.de

License: MIT License

Kotlin 1.90% Java 98.10%
testing stubbing mocking integration-test java junit stub stubbed-methods stubbing-framework compound-test

deep-sampler's Introduction

DeepSampler

Version 2.1.0 - For older versions see 2.0.0, 1.1.0

Build & Test Coverage Bugs Code Smells Maintainability Rating Vulnerabilities

Build integration tests with JUnit and DeepSampler!

DeepSampler is an advanced testing library designed for large software components that require high-quality testing. Unlike traditional unit-testing, DeepSampler's component-testing approach enables you to test several integrated classes within a component, ensuring proper integration while isolating the component from the larger application. With its powerful features, DeepSampler simplifies the process of injecting stubs into isolated components and ๐ŸŽฅ recording ๐Ÿงช stub data at runtime from real-life examples.

This library is perfect for developers who need to ensure their software is of the highest quality and is well-suited for a wide range of applications. Give DeepSampler a try and take your testing to the next level!

Let's say, we wanted to test a compound consisting of numerous classes and somewhere deep inside the compound is one class, a DAO, that reads data from a Database:

A DAO somewhere inside a compound reads data from a database

We don't want to access the database during testing. So we mark the methods, that would access the db, as "stubbed" using DeepSampler. If we run the test with DeepSampler in recording-mode, every call to the marked methods will be intercepted and all data, that was passed to it, or returned by it, is recorded. The recorded data, the sample, will be saved to a JSON-file.

All calls to the DAO get intercepted and parameters and return values are recorded

As a short appetizer, this is how we tell DeepSampler to attach a stub to the method load() in all instances of MyDao:

@PrepareSampler
private MyDao myDaoSampler;
...
PersistentSample.of(myDaoSampler.load(Matchers.anyInt()));

If we repeat the test with DeepSampler switched to player-mode, the marked method will not be called anymore. Instead, a recorded sample from the JSON-file will be returned. If the method is called with particular parameters, DeepSampler looks for a sample, that has been recorded with the same parameters.

Only samples from the previous recording are returned by the stub

๐ŸŽ“ Examples

We have a second repo where we collect runnable examples with detailed explanations: DeepSampler Examples ๐Ÿš€.

Quickstart

The following tutorial demonstrates how to use DeepSampler with JUnit5 and Guice.

Installation

We use Maven to build the example. So, as a first step, we add the following dependencies to our pom.xml:

<dependency>
   <groupId>de.ppi</groupId>
   <artifactId>deepsampler-junit5</artifactId>
   <version>2.1.0</version>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>de.ppi</groupId>
   <artifactId>deepsampler-provider-guice</artifactId>
   <version>2.1.0</version>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>de.ppi</groupId>
   <artifactId>deepsampler-junit-json</artifactId>
   <version>2.1.0</version>
   <scope>test</scope>
</dependency>

The Testee

Let's say, we have a GreetingService that would create greeting messages for particular persons. We know only the ID of the person we want to greet, so the GreetingService needs to lookup the person somewhere (e.g., DB). This is done by a PersonService, which provides a method getName(personId). getName() in turn, would use a PersonDAO to load the Person from a Database.

Now we want to write a test for GreetingService and we want PersonService to be a part of the tested compound, so we cannot simply mock PersonService. Instead, we want to stub the PersonDAO in order to be independent of the database.

JUnit Test

We begin by setting up a JUnit Test. In general this will be an ordinary JUnit Test, but we use the DeepSamplerExtension to activate the DeepSampler environment:

@ExtendWith(DeepSamplerExtension.class)
class GreetingServiceTest {
   ...
}

๐Ÿ”Ž Note DeepSamplerExtension basically provides annotations as a means of convenience for most common use cases. However, DeepSampler can be used without this extension and even outside of JUnit Tests. (See Define a Sampler by API)

The actual stubbing is done by an aop-provider that uses - in this case - Guice aop. It is activated by passing the DeepSamplerModule to guice. In real life we would most likely have a Guice module for the testee, and we would want to combine the testee's module with DeepSamplerModule, however for the sake of simplicity we use DeepSamplerModule directly for now, when we tell Guice to inject members into our test class:

@BeforeEach
void injectWithGuice() {
   Guice.createInjector(new DeepSamplerModule()).injectMembers(this);
}

Define a stub

First we need a Sampler which serves as a model, that describes which methods should be stubbed and what the stub should do. The Sampler is an instance of the class, that we want to stub. We define the Sampler by adding a property with the type of the stubbed class and add the annotation PrepareSampler to it:

@ExtendWith(DeepSamplerExtension.class)
class GreetingServiceTest {

   @PrepareSampler
   private PersonDao personDaoSampler;
   ...
}

Inside a concrete test method we would now use the Sampler to define a stub:

@Test
void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));
   ...
}

This means, the method PersonDao::loadPerson will be stubbed. If loadPerson is called with the parameter 1 the stub will return the Sample new Person("Sarek"). Otherwise, the original method will be called. The stub applies to all instances of PersonDAO, no matter where the instances occur. It will be active for the lifetime of the test method, so each test method can declare its own stubs.

That's it, we can now call the testee and check the results as usual:

@Test
void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));
                                                    ๐Ÿ‘‡
   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
}

๐Ÿ”Ž Note Just to see that the stub is actually doing something, we could clear all stubs and repeat the assertion. Now the original method is used again, and we get another return value:

void greetingShouldBeGenerated() {
   Sample.of(personDaoSampler.loadPerson(1)).is(new Person("Sarek"));

   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
             ๐Ÿ‘‡
   Sampler.clear();
                          ๐Ÿ‘‡
   assertEquals("Hello Geordi La Forge!", greetingService.createGreeting(1));
}

Define a Sampler by API

If you want to use DeepSampler outside of JUnit or don't want to use the annotations, it is possible to create a Sampler using the API:

PersonDao personDaoSampler = Sampler.prepare(PersonDao.class);

SamplerFixtures

Usually it will not be enough to write just one test for a particular compound like the GreetingService, so it would be tedious to repeat the definition of stubs in each test case. To ease that, SamplerFixtures can be used to define reusable stubs. You can also think of a SampleFixture as a definition of a test compound, because the stubs would isolate the compound from the environment in order to make it testable:

public class GreetingServiceCompound implements SamplerFixture {
   @PrepareSampler
   private PersonDao personDaoSampler;

   @Override
   public void defineSamplers() {
      Sample.of(personDaoSampler.loadPerson(Matchers.anyInt())).is(new Person("Sarek"));
   }
}

Once we have such a SamplerFixture, we can bind it to a test case using the annotation @UseSamplerFixture like so:

@Test
@UseSamplerFixture(GreetingServiceCompound.class)
void greetingShouldBeGenerated() {
   ...
}

The stubs, that are defined by the SamplerFixture, are now active for the time the method runs.

Persistent Samples

For the sake of understandability, the tested compound in this example is fairly simple. However, DeepSampler was especially designed for complex compound tests with quite a lot of stubs which also might return extensive Samples. In these cases we would not want to have big sets of test data (Samples) in JUnit Test classes, we would rather separate test data from test logic. And possibly more important, we would not want to write such extensive Samples by hand. To ease this, DeepSampler can save and load Samples from JSON-files.

Record a JSON-Sample

All calls to the DAO get intercepted and parameters and return values are recorded

In order to save Samples in a JSON-file, we first need to define which methods should be stubbed and which methods should be recorded. This is - again - done using SamplerFixtures. In contrast to the example above, we now need to define the Sampler slightly different:

     ๐Ÿ‘‡                                                              ๐Ÿ‘‡               ๐Ÿ‘‡
PersistentSample.of(personDaoSampler.loadPerson(PersistentMatchers.anyRecordedInt()))   ;

Persistent Samples are defined using PersistentSample and we don't need to define a concrete Sample using is() anymore, since this value will be provided by the JSON-File. Additionally, we can now use special parameter matchers for persistent samples. These new matchers honor the fact, that we now want to match any parameters that have been recorded, instead of explicitlty hard-coded parameter values. Second we need to tell DeepSampler to record all Data, that flows through the stubbed methods. This is simply done by adding the annotation @SaveSamples to the test method.

@Test
@SaveSamples
@UseSamplerFixture(GreetingServiceCompound.class)
void recordSamplesToJson() {
   greetingService.createGreeting(1);
}

When we run this test, the stubs will call the original methods and additionally record the parameters and return values for each call. The recorded data is then saved to a JSON-file.

By default @SaveSamples saves the JSON-file in a folder corresponding to the package of the current test case. The filename is created using the class name, and the method name of the package. In this case we would get a file named ./de/ppi/deepsampler/examples/helloworld/GreetingServiceTest_recordSamplesToJson.json.

Load a JSON-Sample

Only samples from the previous recording are returned by the stub

Finally, we can use a SamplerFixture, and a JSON-file to build a test case. A JSON-file can be loaded using the annotation @LoadSamples:

@Test
@LoadSamples
@UseSamplerFixture(GreetingServiceCompound.class)
void loadSamplesFromJson() {
   assertEquals("Hello Sarek!", greetingService.createGreeting(1));
}

By default LoadSamples searches for the JSON-file on the filesystem. The file name is created using the full qualified class name, and the method name of the package. In this case DeepSampler would try to load a file named ./de/ppi/deepsampler/examples/helloworld/GreetingServiceTest_loadSamplesFromJson.json.

Scopes

DeepSampler is by default Thread-scoped. So Samples, that have been defined in one Thread, are available only in this particular Thread.

You can change the Scope using Execution::setScope. DeepSampler comes with two predefined Scopes:

  • ScopeType#THREAD: Samples are Thread-exclusive, this is the default.
  • ScopeType#SINGLETON: The same Samples are available across the entire VM and all Threads share the same Samples.

๐Ÿ”Ž Note the Scope must be changed before any Samples have been defined.

The following line would make all Samples available across all Threads:

    Execution.setScope(ScopeType.SINGLETON);

Authoring custom persistence extensions

The format, in which samplers are recorded, is by default JSON. However, you can change the format by writing your own persistence-module. Let's say we wanted to create a persistence layer that is capable of writing YAML. Then we would have to implement a model like this:

  graph TD;
      A["MyTest#myTestMethod()"]-- Annotated by -->B["@SaveYamlSamples"]
      B-- Annotated by -->C["@UseSourceManagerForSaving"];
      C-- creates -->D[YamlSourceManagerFactory];
      D-- configures -->E[YamlSourceManager]
      A-- records samples with -->E;
      E-- writes -->F["sample.yaml"];
  • MyTest#myTestMethod() is an example for a test-method that is supposed to record a sample-file in YAML-format (or any other format, depending on your extension)
  • @SaveYamlSamples is a custom annotation that is used as a command to tell DeepSampler, that samples should be recorded. This annotation may be named freely, and it may have all kinds of properties, that can be used by the custom extension to configure the recording.
  • @UseSourceManagerForSaving is a meta-annotation that tells DeepSampler, that the annotated annotation is a command for saving samples.
  • YamlSourceManagerFactory is referenced by @UseSourceManagerForsaving and it is used to create and configure a SourceManager. It implements the interface SourceManagerFactory. The configuration should be done using custom annotations. There is a convenience-method that is able to load annotations from various places, like the test-method itself, or a SamplerFixture: JUnitSamplerUtils#loadAnnotationFromTestOrSamplerFixture
  • YamlSourceManager is the class that is finally able to write the YAML-file. It implements the interface SourceManager. This class is probably the most complex class to implement, since it needs to translate the ExecutionInformation to a persistable format. ExecutionInformations contain all data that is collected during the execution of stubbed methods.

Loading samples is implemented very similar. The mentioned interfaces define methods for loading and saving samples. The annotation, that commands DeepSampler to load samples, is marked by the meta annotation @UseSourceManagerForLoading

We recommend placing the extension in two modules:

  1. persistence-module: This module contains all code that is related to saving and loading samples independent of JUnit. It should be possible to use the persistence without annotations or JUnit. The SourceManager -implementation and all it's utilities belong to this module.
  2. junit-configuration-modul: A module that contains all code, that is necessary to use the SourceManager in JUnit-tests together with annotation-based-configuration. The custom-annotations like @SaveYamlSamples and the SourceManagerFactory -implementation belong to this module.

License

DeepSampler is made available under the terms of the MIT License (see LICENSE.md).

Copyright 2022 PPI AG (Hamburg, Germany)

deep-sampler's People

Contributors

chb-ppi avatar dependabot[bot] avatar janschankin avatar kennymck avatar rcschrg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

chb-ppi skrilagg

deep-sampler's Issues

Is it possible to use shorter and thus better readable methodnames in JSON?

Right now we sometimes have quite long and hard to read method names in the JSON-Files. This becomes tedious if the method names are slightly refactored, so that the names in the JSON files need to be changed too. If the change is not done together with the original refactoring but instead as part of a bugfix a few days later, or by another person that doesn't now anything about the refactoring, it becomes hard to find , or even notice the difference.

The names are this extensive, because they must be distinct. Maybe this can be circumnavigated by checking if the name already exists in the SampleRepository. If this is not the case a short name can be used. Otherwise the extensive distinct name must be used.

Is this possible?

Redesign of plugin interface

The plugin interface is not consistently. It is mostly designed around the actual use-case. Futhermore it is in my point of view not save to add a additional plugin. The plugins are hardcoded and have to be in a specific order.

There had to be a standartizied Plugin Interface and a not order dependend possibility to add new plugins.

Matchers are applied to wrong methods

Samplerepository::findAllForMethod applies the mathers for the parameter values even though the type and/or the method did not match. This might lead to ClassCastExceptions if the type of the actual parameter value and the type of the method's parameter don't fit together.

Order of loading JSONs and defining Samplers is messed up

IF we want to load JSONS and define Samplers for void methods, we currently have to do something like this:

  1. Sampler.of(dao.loadStuff());

  2. load Json

  3. Sampler.of(dao.save()).doesNothing()

So the definition of Samplers must be before and after loading the json. This is a bug.
I believe this is caused by

Introduction of a simplyfied BeanConverterExtension

BeanConverterExtension has grown in the past. Although this was necessary in order to persist complex objects (i.e. generic Maps or Lists), we believe that the majority of future extensions will be simpler. Therefore we need a simplified version of BeanConverterExtension. For instance, many Extansion might be able to work without generics, so ParameterizedType might be excludable in the simplified version.

PersistentBeanFactory::createValueFromPersistentBean can't handle Arrays

Hi,

when loading Data that contains an array as a response an InstantiationException is thrown.
One workaround might be creating an extension for that, but this might be core functionality the factory should provide.

Instead of a stracktrace, a simple Unit Test to clarify:

@Test
@DisplayName("createValueFromPersistentBean can handle Arrays")
void createValueFromPersistentBean() {
    PersistentBeanFactory factory = new PersistentBeanFactory();
    
    PersistentBean bean = new DefaultPersistentBean();
    bean.setValues(Collections.emptyMap());
    
    Assertions.assertDoesNotThrow(() -> factory.createValueFromPersistentBean(bean, IOneInterface[].class));
}

interface IOneInterface {
    
    void sampleMethod();
}

best regards,
Michel Pittelkow

Tutorial: How to setup a basic JUnitTest

We need a tutorial that explains how a JUnitTest using DeepSampler would look like. The artciel explains

  • The JUnit5 Extension
  • The Junit4 Rule
  • DeepSamplers various annotations
  • SpringConfig with the Spring AOP-Provier
  • GuiceConfig with the Guice AOP-Provider

The tutorial should have examples for all 4 environment variations using the docsify-tabs-plugin.

Startingpoint is here:
https://ppi-ag.github.io/deep-sampler/#/setup-junit/

PersistentBeanFactory::getAllFields maybe should be extensible

Hi,

I came across the situation that many of our bean classes throw an exception when loading. Storing works fine.
This happens because of final fields inside the bean class and no fitting constructor. This behavior would mean, that I have to create a lot of boilerplate code for de-/serializing these objects when using deepsampler.

This brought to my mind, that maybe getAllFields should be extensible. With that I could e.g. write my own logic on which fields should be serialized in a more generic way, without having to declare BeanFactoryExtensions for all cases.

If this is not the way deepsampler should work -> Alternative:

  • Available constructors should get analyzed to identify which fields are needed and which can be omitted. Maybe this behavior could be summarized in a configuration of deepsampler. Something like "strict, lenient".

Please let me know if this makes sense for you or if I'm missing something here.

Best regards,
Michel Pittelkow

Throw Exception if Sample from file has no matching Sampler

Samples in a JSON-file still need a matching Sampler. If the file contains a Sample for a method that doesn't have a corresponding Sampler, DeepSampler would silently filter the Sample from the file. This results in an unspecific error because a Sample is not used in the test and the original method is called. This error is currently hard to find. It would be better to throw an Exception if the file contains a Sample that has no corresponding Sampler. This Exception should explain which Sample had no Sampler in an easy to unterstand manner.

The code in question is here:

// When there is no matching JointPoint, the persistentJoinPointEntity will be discarded

Tutorial: How to define samples

We need an article that explains the Sampler API in detail. Since this is the core of DeepSampler we should design this article thoroughly.
The article should provide in-depth-information but it should also be easy to search in it, so that it can be used to quickly look things up.
The search-plugin of docsify should be able to find things in this article.

The article should not describe stubbing using the recorder and player, that will be another chapter.

https://ppi-ag.github.io/deep-sampler/#/stubbing/

Improve Errormessage

PersistentbeanFactory::initiateUsingMatchingConstructor throughs an Exception if it is not possible to instantiate an Object using a constructor and if the object's fields are final. The corresponding Exception's message is hard to unterstand (bad english).

Don't save byte[] as base64

Jackson saves byte[] in base64 format. According to Stackoverflow this might have been done to save space on disk. But in our case we often want to compare the byte[] in our .json to a byte[] in the debugger. This is e.g. the case if a byte[] parameter has changed so that a Sample is not found anymore.

So we should provide a custom serializer/deserialzer for Jackson that encodes byte[] in a readable form. This serializer/desrialzer should be default.

Postprocessor for Samples comming from persistent JSON-Files

Sometimes methods change their parameter values (i.e. "return parameters" in clean code terms), even though this is considered to be a code smell. DeepSampler has no means to handle that, yet. A simple solution could be a postprocessor that runs after a stubbed method, or after a Sample has been retrieved from a JSON-file. That postprocessor could then implement arbitrary logic to enhance the return parameter.

Check for non-final Classes prevents capturing proxied services.

Hey all,

I just wanted to let you know, that proxied services cannot be recorded when using something like "target(my.package.MyService)" in a Pointcut when the check "!within(is(FinalType))" is present, see:

I had to create a local copy without that condition to get it working.

Best regards
Michel Pittelkow

Enhancement: Add support to configure if the method suffix should be used for created JSON file name

Hi all,

I am using version 1.1.0 and therefore had to add some functionality, that in parts is already added for version 2.x.

Part of this is the ability to configure a base repository for stored files, which I just noticed you already implemented for the next version.
Another addition I added is the ability to configure if the name of the testMethod should be used in the generated json file name.

Let me explain, why I added this:

  • When creating deepsampler tests I start with a SaveSamples-Test with the name of the service method for example.
  • After that I create another LoadSamples-Test which and comment out the Test Annotation on the LoadSamples method.
  • The default behaviour now would be that I have to either give both methods the same name or set the source name on the LoadSamples-Test everytime.

For convenience I added another configuration value to the Annotation UseSamplerFixture, which is then used inside the JUnitPluginUtils. Let me show you with the following:

public static void saveSamples(final Method testMethod) {
	final SaveSamples saveSamples = testMethod.getAnnotation(SaveSamples.class);

	if (saveSamples == null) {
		return;
	}

	final JsonSourceManager.Builder persistentSampleManagerBuilder = loadBuilder(
			saveSamples.persistenceManagerProvider());

	final String fileName = getFilePath(saveSamples, getFixture(testMethod), testMethod);

	PersistentSamplerHolder.source(persistentSampleManagerBuilder.buildWithFile(fileName));

	addExtensions(testMethod);

	PersistentSamplerHolder.getSampler().record();
}

private static String getFilePath(LoadSamples loadSamples, UseSamplerFixture fixture, Method testMethod) {
	return getFilePath(loadSamples.file(), fixture, testMethod);
}

private static String getFilePath(SaveSamples saveSamples, UseSamplerFixture fixture, Method testMethod) {
	return getFilePath(saveSamples.file(), fixture, testMethod);
}

private static String getFilePath(String path, UseSamplerFixture fixture, Method testMethod) {
	String basePath = fixture != null ? fixture.repositoryBase() : "";
	String file = StringUtils.defaultIfEmpty(path,
			getDefaultJsonFileNameWithFolder(testMethod, withMethod(fixture)));

	return Paths.get(basePath, file).toString();
}

private static boolean withMethod(final UseSamplerFixture fixture) {
	return Optional.ofNullable(fixture).map(anno -> anno.withMethod()).orElse(false);
}

private static String getDefaultJsonFileNameWithFolder(final Method testMethod, boolean withMethod) {
	return testMethod.getDeclaringClass().getName().replace(".", "/")
			+ (withMethod ? ("_" + testMethod.getName()) : "") + ".json";
}

Let me hear your thoughts on this.

Cheers,
Michel

Seperation of marking the sample for persistence and stubbing the sampled method

Currently we use the same API for marking methods and for creating stubs:
// Mark method so it can be used to record/load the method calls
Sample.of(testee.myTestMethod()); // no is/answer -> marked for persistence
Sample.of(testee.myTestMethod()).is("Hello World"); // creating stub to be returned when a real call appears. -> no persistence

This might be confusing when using both ways in a single test. So we should divide these apis to make transparent to the user what is happening.

Add support for Optional

Currently Optionals cannot be persisted. An attempt to do this results in the following Exception:

de.ppi.deepsampler.persistence.error.PersistenceException: The type class java.util.Optional includes at least one final field. Therefore we tried to automatically detect a constructor accepting all field values, but weren't able to find any. If you still want to transform the bean you have to implement a BeanFactoryExtension which is able to construct the desired type class java.util.Optional.

Since Optional is part of standard Java and quite common, DeepSampler should bring it's own BeanFactoryExtension, so that no custom implementations are necessary.

Tutorial: How to install DeepSampler

We need to write a chapter on how to install DeepSampler in all supported (by default) environments:

  • JUnit 5 & Guice
  • JUnit 5 & Spring
  • JUnit 4 & Guice
  • JUnit 4 & Spring
    Once for Maven and for Gradle.

We have a fist starting point here:
https://ppi-ag.github.io/deep-sampler/#/installation/

The article should also give an overview about DeepSampelrs various modules. In most cases users will only have to work with the top dependencies and everything else will be loaded automaticly as transient dependencies by maven/gradle. But sometimes some deeper knowledge might be usefull, at least, if someone wants to build extensions.

Map<Object, Object> should be persistable

We are unable to persist Maps which have non primitve types (i.e. their wrapper types) as key. We need a new BeanConverterExtenstion that is able to convert and revert Maps of the type Map<Object, Object> in a way that the Objects are converted to PersistentBeans.

We are currently able to persist Map<T, Object> where T is any wrapper of a primitve type. This is done by MapPrimitiveKeyExtension. This can be used as a template.
https://github.com/ppi-ag/deep-sampler/blob/645e0ab8f4775d194d6c90a54a94cd3e72cf8ca9/deepsampler-persistence/src/main/java/de/ppi/deepsampler/persistence/bean/ext/MapPrimitiveKeyExtension.java

MapPrimitiveKeyExtension converts the key to a String, so that the key can be used as a property name in JSON:

{
   "key": "value"
}

There is no nice way to convert complex objects to Strings that could be used as a key in the same way, so we need to find another way to express the key in a persistable way. One way to do it, would be to convert the Map in a List which has a tuple as entry. The tuple would then contain the key and the value.

Verify does'nt discriminate between different parameters

Verify should be able to verify calls depending in the parameters. We have a testcase where verify counts all calls of a method together even though the calls were done using different parameters.

The testcase can be found in the branch bug-verify:

public void verifyTwoCallsOnSameMethodWithDifferentParameters() {

CollectionExtension doesn't allow subclasses without generic type parameters

CollectionExtension is using generic type parameters to determine the type of the objects in the collection. This type is used to instantiate the entries of the Collection during deserialization.

Currently CollectionExtension runs on a NullPointerException during recording, if a subtype of Collection is saved, that doesn't provide a generic type argument. The NullPointerException doesn't give any explanation whats wrong, and how to solve the problem. This needs to be changed.

There are several thinkable solutions:

  • Accepting the limitations and throwing an exception with a helpful message
  • Accepting the limitations and preventing the usage of the extension by checking the number of generic type parameters in isProcessable(...)
  • Adding an overridable mehtod to CollectionExtension so that users can provide the type on their own without the need to rewrite the whole extenstion.
  • Writing the type into the JSON.
  • Scanning the inheritance chain to detect the parameter in super types.

ParentPath is missing

If JsonSourceManager.builder().buildWithFile(path) path does not contain a folder (so only a filename) we get a NullPointerException in JsonRecorder::record or more precisly in Files::provider.

Add logging

AOP comes with a downside: If an aspect is not called due to a bug, this bug is not visible, since no code is executed, that could notice the bug and throw an Exception, or print an error message.
As a result, DeepSampler tends to do nothing, if some configuration errors exist. This is quite hard to debug. This could be eased by logging.

Since DeepSampler is an API that must not force the usage of a specific logging API, we must find a possibility to allow a form of logging, that fits into an existing logging environment, and comes without additional dependencies.

Redundant Scope-interface

We have three methods to change the Scope:

  1. Execution#setScope
  2. SampleRepository#setScope
  3. ExecutionRepository#setScope()

The first one is an abstract Version that uses Enums to set the scope and it sets the scope for both, SampleRepository and ExecutionRepository.

Number 2 and 3 are on a lower level of the API and are used to set the actual scope-implementation to the repositories that need scoping.

Users cannot understand which of these methods should be used to set the scope. Therefore we need to clarify the API.

Allow Polymorphic persistenence

When DeepSampler wants to deserialize an Object, the concrete type of the object must be known, so that a new instance of that type can be created.

There are two cases where different deserialization approaches are used:

  • Serialization and Deserialization is completely done by the underlying persistence framework (Jackson by default)
  • Serialization and Deserialization is done using PersistentBeans by DeepSamplers PersistentBeanConverter`.

In the first case type information is completely handled by the persistence framework (Jackson).

In the second case DeepSampler uses the declared return type of the method that is supposed to return the persistent object. Now, there are circumstances where this doesn't work. In general, this is always the case if the actually returned object is a sub type of the declared return type. E.g. in many cases this happens if the declared return type is an interface.

That's why we need to add a type information to PersistentBean in all cases where the original type cannot be determined by the declared return type of the sampled method.

Allow SampleDefinitions across different Threads

If DeepSampler is used inside of an ApplicationServer and the definition of Samples and saving recorded data to a file is should be done in separated Threads, we need a possibility to keep the SampleRepository alive across Thread borders.

Usually we enforce thread safety using ThreadLocals. This should be configurable.

Throw an Exception if a SampledMethod was found, but no fitting parameters where available

Sometimes Samples are not found because the expected parameters have changed. Currently DeepSampler would treat this situation the same way as if no Sampler (no fitting method) was found at all. But in reality the first situation may occur because the tested code has changed. In this case DeepSampler would simply call the stubbed code. It would be better if DeepSampler would throw an Exception if the a Sample for a called Method exists, but no fitting parameter could be found.

JsonRecorder missing charset in OutputStreamWriter, JsonLoader uses hardcoded UTF8

Hi,

just came across an issue with charset/encoding of recorded data:

return createObjectMapper().readValue(new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)), JsonSampleModel.class);

reads the files with UTF8 but in

final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(persistentResource.writeAsStream(StandardOpenOption.TRUNCATE_EXISTING,

no charset is configured in the OutputStreamWriter.

We use Cp1252 most of the time and when loading the data from the json file, it's corrupted then.
Maybe I'm missing a configuration that I can adjust.

Regards,
Michel

simplification of combo-matchers

When DeepSampler tries to find a sample for a stubbed method in a JSON-file, the recorded parameters from the JSON-file are compared to the actual parameters, with which the stubbed method is actually called during the test. The comparison, is by default, done using equals()-methods. However, some parameter objects don't override equals() and it is sometimes not possible to add an equals()-method to the object. For cases like this, the PersistentMatchers#combo() method was introduced. It allows to add a replacement for the equals() method. But the method was designed from the internal perspective instead of from the user's perspective, hence the method is hard to understand. The reason for that is the internal distinction between matchers that are used during recording and replay. Combo-matchers allow to define both variants freely. But this is seldomly usefull, so the added complexity is not helpful.

CustomMatchers for Deserialization

Deserialized SampleDefinitions don't honor custom matchers even though they are known by the SampleDefinition that is defined by Samper.of(...). So currently deserialized Sampler use the EqualsMatcher only.

Filter redundant Samples while recording

If a stubbed method is called multiple times with the same parameter each call is saved in the json file leading to redundant data. A filter option would be nice to get cleaner files.

Allow configuration of another SourceManager using an annotation

Currently we can only define another Sourcemanager using the low-level-api as in

PersistentSampler.source(JsonSourceManager.builder().buildWithFile(pathToFile))

This also means, that persistence related annotations cannot be used at all, if another SourceManager must be used. Therefore we need a new annotation that configures the SourceManager.

Remove dependency to comons-lang

comons-lang is propably a unessecary dependency which might leyd to problems if a customer doesn't have the proper version in a mirrored maven repo server

Configurable path for SaveSamples/LoadSamples annotation

If we define a target path for saving/loading samples we currently have to define the path including the filename. Also we have to define this in every single test-method. In reality we mostly want to define one path once without the filename, as the filename can easily be generated.

Goal:

  • Configurable file path for saving/loading .json files
  • This path should have the scope of the whole test-class

EqualsMatcher does not work for String.class

Hi,

after I finished working on SaveSamples-part of a service execution, I started with the LoadSamples-part.
This quickly throw me an Matcher-Error for a call like Sample.of(service.doStuff(Matchers.equalTo("MyString"))); as the EqualsMatcher does not allow a type without an equals-implementation.

As a workaround I then used Matcher.matcher(param -> "MyString".equals(param)).

When taking a look at e.g. hamcrest as matcher library, this is working with String as well.
I thought about implementing a hamcrest-to-ParameterMatcher wrapper but wanted to let you know first as you maybe want to do something about this inside deepsampler.

Best regards,
Michel Pittelkow

Record return parameters

Sometimes methods change parameter values instead of returning results as return values (i.e. return parameter in the terms of clean code). We need som means to record and replay that using JSON-persistence.

Enumerations are converted to PersistentBeans

In Version 1.1.0 enumerations are converted to PersistentBeans. Recording enums in this way is possible, but the results don't make sense. Loading and replaying such enums is not possible. An Excwption is thrown since DeepSampler tries to call a constructor on individual enums.

A simple workaround is a PersistenceBeanConverter (PersistenceFactoryExtension in older versions) that skips the conversion, so that the underlying json-api (Jackson by default) handles the enum.

EqualsMatcher has a too strict check for equals() methods

The EqualsMatcher checks if the object that is beeing matched has a equals() that is not just the implementation from Object. But it expects that the equals() is implemented in the top subclass. If equals() has been implemented on some parent class that is not Object, the check would fail even though a custom equals() method exists. This is too strict, so the check should allow every equals() as long as it is not the one from Object.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.