Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add explanation for testing subgraph extended classes #564

Merged
merged 4 commits into from
Apr 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/subgraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,117 @@ To stop:
yarn stop:dev
```

### Writing the tests

In the tests, most of the time, one needs to do:

- Create an event object.
- Call the handler with this event object.
- Check whether the handler successfully created an entity with the values that came from the event.

The simple code could look like:

```js
let event = createNewProposalCreatedEvent(
PLUGIN_PROPOSAL_ID,
ADDRESS_ONE,
START_DATE,
END_DATE,
STRING_DATA,
[],
ALLOW_FAILURE_MAP,
CONTRACT_ADDRESS
);

_handleProposalCreated(event, daoEntityId, STRING_DATA);

assert.fieldEquals('AddresslistVotingProposal', PROPOSAL_ENTITY_ID, 'id', PROPOSAL_ENTITY_ID);
assert.fieldEquals('AddresslistVotingProposal', PROPOSAL_ENTITY_ID, 'dao', daoEntityId);
// ...more fieldEqual calls
```

As one can see, most of the time, event object would be created with some default values, such as in the example. i.e
ADDRESS_ONE, START_DATE, END_DATE,.... So prior to extended class approach, we ended up having default values in the tests themselves, and then calling `assert.fieldEquals` multiple times to check all the fields were stored successfully on the entity. Not only this caused code to become larger and larger, but default values were scattered throughout the whole test codebase. Even though, these default values could be hidden/abstracted away from the main test files, they still have to live somewhere and there's no appropriate files for them to be included in.

Aragon currently uses extended class approach where we dynamically and programatically create extended class that extends from the actual entity classes and append `Extended` to the name. For example: if the entity is called `TokenVotingProposal`, our script generates a class `ExtendedTokenVotingProposal` that extends from `TokenVotingProposal` and adds a couple of helper functions into this class. Most importantly, default values are included in such classes and are not scattered around, ensuring that they only live in their own respective places. Not only that, script also adds `assertEntity` to the class that loops through all the properties and checks whether the entity with those values are included in the database.

```js
export class ExtendedTokenVotingProposal extends TokenVotingProposal {
withDefaultValues(): ExtendedTokenVotingProposal {
this.id = PROPOSAL_ENTITY_ID;
this.dao = DAO_ADDRESS;
this.plugin = Address.fromHexString(CONTRACT_ADDRESS).toHexString();
this.pluginProposalId = BigInt.fromString(PLUGIN_PROPOSAL_ID);
this.creator = Address.fromHexString(ADDRESS_ONE);
this.open = true;
this.executed = false;
this.votingMode = VOTING_MODE;
/// some more properties
}

createEvent_ProposalCreated(
actions: ethereum.Tuple[],
description: string = STRING_DATA
): ProposalCreated {
let event = createNewProposalCreatedEvent(
this.pluginProposalId.toString(),
this.creator.toHexString(),
this.startDate.toString(),
this.endDate.toString(),
description,
actions,
this.allowFailureMap.toString(),
this.plugin
);
return event;
}

assertEntity(debug: boolean = false): void {
let entity = TokenVotingProposal.load(this.id);
if (!entity) throw new Error('Entity not found');
let entries = entity.entries;
for (let i = 0; i < entries.length; i++) {
let key = entries[i].key;

let value = this.get(key);

assert.fieldEquals('TokenVotingProposal', this.id, key, value.displayData());
}
}
}
```

There're a couple of things we achieved:

- The default values are not scattered anymore and only live in their appropire extended entity classes.
- There's no need to call `fieldEquals` multiple times for each field in the tests. Simply calling `assertEntity` is enough.
- Creating the events is much simpler, because it uses the same default values that live in the same class which brings much more safety.
- We can easily test to see if the saved entity in the database (fetched by the `id` field) matches the fields _currently_ on the Extended Entity. These will be the default values unless you overwrite them (see below for an example)

novaknole marked this conversation as resolved.
Show resolved Hide resolved
Usage of the extended class is the following:

```js
let proposal = new ExtendedTokenVotingProposal().withDefaultValues();
let event = proposal.createEvent_ProposalCreated(actions, STRING_DATA);
_handleProposalCreated(event, proposal.dao, STRING_DATA);
// If the event uses non-default values for a couple of fields
// Update the proposal's fields with those values
proposal.creationBlockNumber = BigInt.fromString(ONE);
proposal.votingMode = VOTING_MODES.get(parseInt(VOTING_MODE)) as string;
// assert
proposal.assertEntity();
```

**How it works behind the hood**

When you have a new entity in the schema, go to `/helpers/method-classes.ts` and add a new class that extends from that entity. Then add the functions that you want this extended class to have.

After running `yarn test`, our script('schema-extender.ts') loops through the classes in method-classes and cleverly generates the `helpers/extended-schema.ts` which can be used in the tests to use Extended classes instead of the original entity classes.

novaknole marked this conversation as resolved.
Show resolved Hide resolved
Setting properties on the extended entity (before saving) will allow you to run `assertEntity` to check that the properties _fetched from the database_ match the properties you've just set, else it will use the default values.

In the above example we are asserting that the `creationsBlockNumber` and `votingMode` are set to specific values, while the rest of the entity will be checked against the defaults.

## Github Labels / Workflows

### Workflows
Expand Down
Loading